diff options
author | John Alhroos <john.ahlroos@itmill.com> | 2011-08-10 07:35:47 +0000 |
---|---|---|
committer | John Alhroos <john.ahlroos@itmill.com> | 2011-08-10 07:35:47 +0000 |
commit | 111d9c2f9a87b4954c30aa5736bf86b9d504bbb1 (patch) | |
tree | 2b5990d8582ea3afb1b6492456fea9b60b0fb3d7 /src/com/vaadin/data/util | |
parent | 44011d913afb6e6131b3ff5c53773f99294b64d6 (diff) | |
download | vaadin-framework-111d9c2f9a87b4954c30aa5736bf86b9d504bbb1.tar.gz vaadin-framework-111d9c2f9a87b4954c30aa5736bf86b9d504bbb1.zip |
- Merged SQLContainer with Vaadin 6.7.
- Updated EasyMock to version 3.0 (SQLContainer requirement)
svn changeset:20252/svn branch:6.7
Diffstat (limited to 'src/com/vaadin/data/util')
38 files changed, 5373 insertions, 0 deletions
diff --git a/src/com/vaadin/data/util/CacheFlushNotifier.java b/src/com/vaadin/data/util/CacheFlushNotifier.java new file mode 100644 index 0000000000..c95378566e --- /dev/null +++ b/src/com/vaadin/data/util/CacheFlushNotifier.java @@ -0,0 +1,88 @@ +package com.vaadin.data.util;
+
+import java.lang.ref.ReferenceQueue;
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.List;
+
+import com.vaadin.data.util.query.FreeformQuery;
+import com.vaadin.data.util.query.QueryDelegate;
+import com.vaadin.data.util.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 {
+ /*
+ * 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/src/com/vaadin/data/util/CacheMap.java b/src/com/vaadin/data/util/CacheMap.java new file mode 100644 index 0000000000..a7a17c6eee --- /dev/null +++ b/src/com/vaadin/data/util/CacheMap.java @@ -0,0 +1,28 @@ +package com.vaadin.data.util;
+
+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/src/com/vaadin/data/util/ColumnProperty.java b/src/com/vaadin/data/util/ColumnProperty.java new file mode 100644 index 0000000000..0deb1a756e --- /dev/null +++ b/src/com/vaadin/data/util/ColumnProperty.java @@ -0,0 +1,241 @@ +package com.vaadin.data.util; + +import java.lang.reflect.Constructor; +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; + } + + public Object getValue() { + if (isModified()) { + return changedValue; + } + return value; + } + + public void setValue(Object newValue) throws ReadOnlyException, + ConversionException { + 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 the type is not correct, try to generate it through a possibly + * existing String constructor. + */ + if (!getType().isAssignableFrom(newValue.getClass())) { + try { + final Constructor<?> constr = getType().getConstructor( + new Class[] { String.class }); + newValue = constr.newInstance(new Object[] { newValue + .toString() }); + } catch (Exception e) { + throw new ConversionException(e); + } + } + + /* + * If the value to be set is the same that has already been set, do + * not set it again. + */ + if (newValue.equals(value)) { + return; + } + } + + /* Set the new value and notify container of the change. */ + changedValue = newValue; + owner.getContainer().itemChangeNotification(owner); + modified = true; + } + + public Class<?> getType() { + return type; + } + + public boolean isReadOnly() { + return readOnly; + } + + public boolean isReadOnlyChangeAllowed() { + return allowReadOnlyChange; + } + + public void setReadOnly(boolean newStatus) { + if (allowReadOnlyChange) { + readOnly = newStatus; + } + } + + public String getPropertyId() { + return propertyId; + } + + @Override + public String toString() { + Object val = getValue(); + if (val == null) { + return null; + } + return val.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/src/com/vaadin/data/util/OptimisticLockException.java b/src/com/vaadin/data/util/OptimisticLockException.java new file mode 100644 index 0000000000..a32994aacb --- /dev/null +++ b/src/com/vaadin/data/util/OptimisticLockException.java @@ -0,0 +1,33 @@ +package com.vaadin.data.util; + +/** + * 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 com.vaadin.addon.sqlcontainer.query.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/src/com/vaadin/data/util/ReadOnlyRowId.java b/src/com/vaadin/data/util/ReadOnlyRowId.java new file mode 100644 index 0000000000..a3c559c26d --- /dev/null +++ b/src/com/vaadin/data/util/ReadOnlyRowId.java @@ -0,0 +1,28 @@ +package com.vaadin.data.util; + +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/src/com/vaadin/data/util/Reference.java b/src/com/vaadin/data/util/Reference.java new file mode 100644 index 0000000000..9e174b4f0f --- /dev/null +++ b/src/com/vaadin/data/util/Reference.java @@ -0,0 +1,53 @@ +package com.vaadin.data.util;
+
+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/src/com/vaadin/data/util/RowId.java b/src/com/vaadin/data/util/RowId.java new file mode 100644 index 0000000000..565161b597 --- /dev/null +++ b/src/com/vaadin/data/util/RowId.java @@ -0,0 +1,78 @@ +package com.vaadin.data.util;
+
+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/src/com/vaadin/data/util/RowItem.java b/src/com/vaadin/data/util/RowItem.java new file mode 100644 index 0000000000..5fae27824d --- /dev/null +++ b/src/com/vaadin/data/util/RowItem.java @@ -0,0 +1,125 @@ +package com.vaadin.data.util; + +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; + } + + public Property getItemProperty(Object id) { + if (id instanceof String && id != null) { + for (ColumnProperty cp : properties) { + if (id.equals(cp.getPropertyId())) { + return cp; + } + } + } + return null; + } + + 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. + */ + public boolean addItemProperty(Object id, Property property) + throws UnsupportedOperationException { + throw new UnsupportedOperationException(); + } + + /** + * Removing properties is not supported. Properties are generated by + * SQLContainer. + */ + 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(":"); + s.append(getItemProperty(propId).toString()); + } + return s.toString(); + } + + public void commit() { + if (properties != null) { + for (ColumnProperty p : properties) { + p.commit(); + } + } + } +} diff --git a/src/com/vaadin/data/util/SQLContainer.java b/src/com/vaadin/data/util/SQLContainer.java new file mode 100644 index 0000000000..9189727de9 --- /dev/null +++ b/src/com/vaadin/data/util/SQLContainer.java @@ -0,0 +1,1587 @@ +package com.vaadin.data.util; + +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 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.query.OrderBy; +import com.vaadin.data.util.query.QueryDelegate; +import com.vaadin.data.util.query.QueryDelegate.RowIdChangeListener; +import com.vaadin.data.util.query.TableQuery; +import com.vaadin.data.util.query.generator.MSSQLGenerator; +import com.vaadin.data.util.query.generator.OracleGenerator; + +public class SQLContainer implements Container, Container.Filterable, + Container.Indexed, Container.Sortable, Container.ItemSetChangeNotifier { + private static final long serialVersionUID = -3863564310693712511L; + + /** 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; + + /** Enable to output possible stack traces and diagnostic information */ + private boolean debugMode; + + /** + * 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} + */ + 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); + } + debug(null, "Row added to DB..."); + return itemId; + } catch (SQLException e) { + debug(e, null); + try { + delegate.rollback(); + } catch (SQLException ee) { + debug(ee, null); + } + return null; + } + } else { + addedItems.add(newRowItem); + fireContentsChange(); + return itemId; + } + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container#containsId(java.lang.Object) + */ + 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 TemporaryRowId)) { + try { + return delegate.containsRowWithKey(((RowId) itemId).getId()); + } catch (Exception e) { + /* Query failed, just return false. */ + debug(e, null); + } + } + return false; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container#getContainerProperty(java.lang.Object, + * java.lang.Object) + */ + 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() + */ + public Collection<?> getContainerPropertyIds() { + return Collections.unmodifiableCollection(propertyIds); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container#getItem(java.lang.Object) + */ + 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} + */ + 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) { + debug(e, null); + try { + delegate.rollback(); + } catch (SQLException e1) { + debug(e1, null); + } + try { + rs.getStatement().close(); + rs.close(); + } catch (SQLException e1) { + debug(e1, null); + } + 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) + */ + public Class<?> getType(Object propertyId) { + if (!propertyIds.contains(propertyId)) { + return null; + } + return propertyTypes.get(propertyId); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container#size() + */ + public int size() { + updateCount(); + return size + sizeOfAddedItems() - removedItems.size(); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container#removeItem(java.lang.Object) + */ + 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) { + debug(null, "Row removed from DB..."); + } + return success; + } catch (SQLException e) { + debug(e, null); + try { + delegate.rollback(); + } catch (SQLException ee) { + /* Nothing can be done here */ + debug(ee, null); + } + return false; + } + } else { + removedItems.put((RowId) itemId, (RowItem) getItem(itemId)); + cachedItems.remove(itemId); + refresh(); + return true; + } + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container#removeAllItems() + */ + 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(); + debug(null, "All rows removed from DB..."); + refresh(); + if (notificationsEnabled) { + CacheFlushNotifier.notifyOfCacheFlush(this); + } + } else { + delegate.rollback(); + } + return success; + } catch (SQLException e) { + debug(e, null); + try { + delegate.rollback(); + } catch (SQLException ee) { + /* Nothing can be done here */ + debug(ee, null); + } + return false; + } + } else { + for (Object id : getItemIds()) { + removedItems.put((RowId) id, (RowItem) getItem(id)); + cachedItems.remove(id); + } + refresh(); + return true; + } + } + + /*************************************************/ + /** Methods from interface Container.Filterable **/ + /*************************************************/ + + /** + * {@inheritDoc} + */ + public void addContainerFilter(Filter filter) + throws UnsupportedFilterException { + // filter.setCaseSensitive(!ignoreCase); + + filters.add(filter); + refresh(); + } + + /** + * {@inheritDoc} + */ + public void removeContainerFilter(Filter filter) { + filters.remove(filter); + } + + /** + * {@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} + */ + public void removeAllContainerFilters() { + filters.clear(); + refresh(); + } + + /**********************************************/ + /** Methods from interface Container.Indexed **/ + /**********************************************/ + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container.Indexed#indexOfId(java.lang.Object) + */ + 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) + */ + 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) + */ + public Object nextItemId(Object itemId) { + return getIdByIndex(indexOfId(itemId) + 1); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container.Ordered#prevItemId(java.lang.Object) + */ + public Object prevItemId(Object itemId) { + return getIdByIndex(indexOfId(itemId) - 1); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container.Ordered#firstItemId() + */ + 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() + */ + 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) + */ + public boolean isFirstId(Object itemId) { + return firstItemId().equals(itemId); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container.Ordered#isLastId(java.lang.Object) + */ + 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[]) + */ + 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) { + debug(e, null); + } + sorters.add(new OrderBy((String) propertyId[i], asc)); + } + } + refresh(); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container.Sortable#getSortableContainerPropertyIds() + */ + 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 { + debug(null, "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; + } + } + + /** + * 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 { + debug(null, "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); + } + debug(null, "Row updated to DB..."); + } catch (SQLException e) { + debug(e, null); + try { + delegate.rollback(); + } catch (SQLException ee) { + /* Nothing can be done here */ + debug(e, null); + } + 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) { + /* The query delegate doesn't support filtering. */ + debug(e, null); + } + try { + delegate.setOrderBy(sorters); + } catch (UnsupportedOperationException e) { + /* The query delegate doesn't support filtering. */ + debug(e, null); + } + int newSize = delegate.getCount(); + if (newSize != size) { + size = newSize; + refresh(); + } + sizeUpdated = new Date(); + sizeDirty = false; + debug(null, "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) { + debug(e, null); + /* 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(); + debug(null, "Property IDs fetched."); + } catch (SQLException e) { + debug(e, null); + try { + delegate.rollback(); + } catch (SQLException e1) { + debug(e1, null); + } + try { + if (rs != null) { + if (rs.getStatement() != null) { + rs.getStatement().close(); + } + rs.close(); + } + } catch (SQLException e1) { + debug(e1, null); + } + 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. */ + debug(e, null); + } + 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); + cachedItems.put(id, new RowItem(this, id, itemProperties)); + rowCount++; + } + } + rs.getStatement().close(); + rs.close(); + delegate.commit(); + debug(null, "Fetched " + pageLength * CACHE_RATIO + + " rows starting from " + currentOffset); + } catch (SQLException e) { + debug(e, null); + try { + delegate.rollback(); + } catch (SQLException e1) { + debug(e1, null); + } + try { + if (rs != null) { + if (rs.getStatement() != null) { + rs.getStatement().close(); + rs.close(); + } + } + } catch (SQLException e1) { + debug(e1, null); + } + throw new RuntimeException("Failed to fetch page.", e); + } + } + + 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) + */ + 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) + */ + public boolean removeContainerProperty(Object propertyId) + throws UnsupportedOperationException { + throw new UnsupportedOperationException(); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container#addItem(java.lang.Object) + */ + 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) + */ + 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) + */ + public Item addItemAt(int index, Object newItemId) + throws UnsupportedOperationException { + throw new UnsupportedOperationException(); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container.Indexed#addItemAt(int) + */ + public Object addItemAt(int index) throws UnsupportedOperationException { + throw new UnsupportedOperationException(); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container.Ordered#addItemAfter(java.lang.Object) + */ + 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) + */ + 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) + */ + 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 class ItemSetChangeEvent extends EventObject implements + Container.ItemSetChangeEvent { + + private ItemSetChangeEvent(SQLContainer source) { + super(source); + } + + 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); + } + } + + public boolean isDebugMode() { + return debugMode; + } + + public void setDebugMode(boolean debugMode) { + this.debugMode = debugMode; + } + + /** + * Output a debug message or a stack trace of an exception + * + * @param message + */ + private void debug(Exception e, String message) { + if (debugMode) { + // TODO: Replace with the common Vaadin logging system once it is + // available. + if (message != null) { + System.err.println(message); + } + if (e != null) { + e.printStackTrace(); + } + } + } + + /** + * 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) { + debug(e, "Setting referenced item failed."); + 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)); + } + +}
\ No newline at end of file diff --git a/src/com/vaadin/data/util/SQLUtil.java b/src/com/vaadin/data/util/SQLUtil.java new file mode 100644 index 0000000000..5426d8d213 --- /dev/null +++ b/src/com/vaadin/data/util/SQLUtil.java @@ -0,0 +1,31 @@ +package com.vaadin.data.util; + +public class SQLUtil { + /** + * 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/src/com/vaadin/data/util/TemporaryRowId.java b/src/com/vaadin/data/util/TemporaryRowId.java new file mode 100644 index 0000000000..ec5ca7f862 --- /dev/null +++ b/src/com/vaadin/data/util/TemporaryRowId.java @@ -0,0 +1,29 @@ +package com.vaadin.data.util; + +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/src/com/vaadin/data/util/connection/J2EEConnectionPool.java b/src/com/vaadin/data/util/connection/J2EEConnectionPool.java new file mode 100644 index 0000000000..789fcb9225 --- /dev/null +++ b/src/com/vaadin/data/util/connection/J2EEConnectionPool.java @@ -0,0 +1,61 @@ +package com.vaadin.data.util.connection; + +import java.sql.Connection; +import java.sql.SQLException; + +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; + } + + 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()); + } + } + + public void releaseConnection(Connection conn) { + try { + conn.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public void destroy() { + dataSource = null; + } + +}
\ No newline at end of file diff --git a/src/com/vaadin/data/util/connection/JDBCConnectionPool.java b/src/com/vaadin/data/util/connection/JDBCConnectionPool.java new file mode 100644 index 0000000000..8f10d7706a --- /dev/null +++ b/src/com/vaadin/data/util/connection/JDBCConnectionPool.java @@ -0,0 +1,38 @@ +package com.vaadin.data.util.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/src/com/vaadin/data/util/connection/SimpleJDBCConnectionPool.java b/src/com/vaadin/data/util/connection/SimpleJDBCConnectionPool.java new file mode 100644 index 0000000000..91da3f1677 --- /dev/null +++ b/src/com/vaadin/data/util/connection/SimpleJDBCConnectionPool.java @@ -0,0 +1,162 @@ +package com.vaadin.data.util.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;
+ }
+
+ 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;
+ }
+
+ 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;
+ }
+
+ 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/src/com/vaadin/data/util/filter/Between.java b/src/com/vaadin/data/util/filter/Between.java new file mode 100644 index 0000000000..a44995b24a --- /dev/null +++ b/src/com/vaadin/data/util/filter/Between.java @@ -0,0 +1,69 @@ +package com.vaadin.data.util.filter; + +import com.vaadin.data.Container.Filter; +import com.vaadin.data.Item; + +public class Between implements Filter { + + private final Object propertyId; + private final Comparable startValue; + private final Comparable endValue; + + public Between(Object propertyId, Comparable startValue, Comparable endValue) { + this.propertyId = propertyId; + this.startValue = startValue; + this.endValue = endValue; + } + + public Object getPropertyId() { + return propertyId; + } + + public Comparable<?> getStartValue() { + return startValue; + } + + public Comparable<?> getEndValue() { + return endValue; + } + + public boolean passesFilter(Object itemId, Item item) + throws UnsupportedOperationException { + Object value = item.getItemProperty(getPropertyId()).getValue(); + if (value instanceof Comparable) { + Comparable cval = (Comparable) value; + return cval.compareTo(getStartValue()) >= 0 + && cval.compareTo(getEndValue()) <= 0; + } + return false; + } + + public boolean appliesToProperty(Object propertyId) { + return getPropertyId() != null && getPropertyId().equals(propertyId); + } + + @Override + public int hashCode() { + return getPropertyId().hashCode() + getStartValue().hashCode() + + getEndValue().hashCode(); + } + + @Override + public boolean equals(Object obj) { + // Only objects of the same class can be equal + if (!getClass().equals(obj.getClass())) { + return false; + } + final Between o = (Between) obj; + + // Checks the properties one by one + boolean propertyIdEqual = (null != getPropertyId()) ? getPropertyId() + .equals(o.getPropertyId()) : null == o.getPropertyId(); + boolean startValueEqual = (null != getStartValue()) ? getStartValue() + .equals(o.getStartValue()) : null == o.getStartValue(); + boolean endValueEqual = (null != getEndValue()) ? getEndValue().equals( + o.getEndValue()) : null == o.getEndValue(); + return propertyIdEqual && startValueEqual && endValueEqual; + + } +} diff --git a/src/com/vaadin/data/util/filter/Like.java b/src/com/vaadin/data/util/filter/Like.java new file mode 100644 index 0000000000..a3cee49f26 --- /dev/null +++ b/src/com/vaadin/data/util/filter/Like.java @@ -0,0 +1,78 @@ +package com.vaadin.data.util.filter; + +import com.vaadin.data.Container.Filter; +import com.vaadin.data.Item; + +public class Like implements Filter { + private final Object propertyId; + private final String value; + private boolean caseSensitive; + + public Like(String propertyId, String value) { + this(propertyId, value, true); + } + + public Like(String propertyId, String value, boolean caseSensitive) { + this.propertyId = propertyId; + this.value = value; + setCaseSensitive(caseSensitive); + } + + public Object getPropertyId() { + return propertyId; + } + + public String getValue() { + return value; + } + + public void setCaseSensitive(boolean caseSensitive) { + this.caseSensitive = caseSensitive; + } + + public boolean isCaseSensitive() { + return caseSensitive; + } + + public boolean passesFilter(Object itemId, Item item) + throws UnsupportedOperationException { + if (!item.getItemProperty(getPropertyId()).getType() + .isAssignableFrom(String.class)) { + // We can only handle strings + return false; + } + String colValue = (String) item.getItemProperty(getPropertyId()) + .getValue(); + + String pattern = getValue().replace("%", ".*"); + if (isCaseSensitive()) { + return colValue.matches(pattern); + } + return colValue.toUpperCase().matches(pattern.toUpperCase()); + } + + public boolean appliesToProperty(Object propertyId) { + return getPropertyId() != null && getPropertyId().equals(propertyId); + } + + @Override + public int hashCode() { + return getPropertyId().hashCode() + getValue().hashCode(); + } + + @Override + public boolean equals(Object obj) { + // Only objects of the same class can be equal + if (!getClass().equals(obj.getClass())) { + return false; + } + final Like o = (Like) obj; + + // Checks the properties one by one + boolean propertyIdEqual = (null != getPropertyId()) ? getPropertyId() + .equals(o.getPropertyId()) : null == o.getPropertyId(); + boolean valueEqual = (null != getValue()) ? getValue().equals( + o.getValue()) : null == o.getValue(); + return propertyIdEqual && valueEqual; + } +} diff --git a/src/com/vaadin/data/util/query/FreeformQuery.java b/src/com/vaadin/data/util/query/FreeformQuery.java new file mode 100644 index 0000000000..2ac67c990f --- /dev/null +++ b/src/com/vaadin/data/util/query/FreeformQuery.java @@ -0,0 +1,450 @@ +package com.vaadin.data.util.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.RowItem; +import com.vaadin.data.util.SQLContainer; +import com.vaadin.data.util.connection.JDBCConnectionPool; +import com.vaadin.data.util.query.generator.StatementHelper; +import com.vaadin.data.util.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} + */ + 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 pagelength 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 com.vaadin.addon.sqlcontainer.query.FreeformQueryDelegate#getQueryString(int, + * int) {@inheritDoc} + */ + @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; + } + + @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.addon.sqlcontainer.query.QueryDelegate#setFilters(java.util + * .List) + */ + public void setFilters(List<Filter> filters) + throws UnsupportedOperationException { + if (delegate != null) { + delegate.setFilters(filters); + } else if (filters != null) { + throw new UnsupportedOperationException( + "FreeFormQueryDelegate not set!"); + } + } + + public void setOrderBy(List<OrderBy> orderBys) + throws UnsupportedOperationException { + if (delegate != null) { + delegate.setOrderBy(orderBys); + } else if (orderBys != null) { + throw new UnsupportedOperationException( + "FreeFormQueryDelegate not set!"); + } + } + + 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!"); + } + } + + 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!"); + } + } + + public synchronized void beginTransaction() + throws UnsupportedOperationException, SQLException { + if (activeConnection != null) { + throw new IllegalStateException("A transaction is already active!"); + } + activeConnection = connectionPool.reserveConnection(); + activeConnection.setAutoCommit(false); + } + + 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; + } + + public synchronized void rollback() throws UnsupportedOperationException, + SQLException { + if (activeConnection == null) { + throw new SQLException("No active transaction"); + } + activeConnection.rollback(); + connectionPool.releaseConnection(activeConnection); + activeConnection = null; + } + + 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 com.vaadin.addon.sqlcontainer.query.FreeformQueryDelegate#getContainsRowQueryString(Object...) + * + * {@inheritDoc} + */ + @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/src/com/vaadin/data/util/query/FreeformQueryDelegate.java b/src/com/vaadin/data/util/query/FreeformQueryDelegate.java new file mode 100644 index 0000000000..c87acecb72 --- /dev/null +++ b/src/com/vaadin/data/util/query/FreeformQueryDelegate.java @@ -0,0 +1,115 @@ +package com.vaadin.data.util.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.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/src/com/vaadin/data/util/query/FreeformStatementDelegate.java b/src/com/vaadin/data/util/query/FreeformStatementDelegate.java new file mode 100644 index 0000000000..0de8677095 --- /dev/null +++ b/src/com/vaadin/data/util/query/FreeformStatementDelegate.java @@ -0,0 +1,54 @@ +package com.vaadin.data.util.query;
+
+import com.vaadin.data.util.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/src/com/vaadin/data/util/query/OrderBy.java b/src/com/vaadin/data/util/query/OrderBy.java new file mode 100644 index 0000000000..d83d3223b0 --- /dev/null +++ b/src/com/vaadin/data/util/query/OrderBy.java @@ -0,0 +1,43 @@ +package com.vaadin.data.util.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/src/com/vaadin/data/util/query/QueryDelegate.java b/src/com/vaadin/data/util/query/QueryDelegate.java new file mode 100644 index 0000000000..95ec89f8c3 --- /dev/null +++ b/src/com/vaadin/data/util/query/QueryDelegate.java @@ -0,0 +1,208 @@ +package com.vaadin.data.util.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.RowId; +import com.vaadin.data.util.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/src/com/vaadin/data/util/query/TableQuery.java b/src/com/vaadin/data/util/query/TableQuery.java new file mode 100644 index 0000000000..c44a748d9a --- /dev/null +++ b/src/com/vaadin/data/util/query/TableQuery.java @@ -0,0 +1,707 @@ +package com.vaadin.data.util.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 com.vaadin.data.Container.Filter; +import com.vaadin.data.util.ColumnProperty; +import com.vaadin.data.util.OptimisticLockException; +import com.vaadin.data.util.RowId; +import com.vaadin.data.util.RowItem; +import com.vaadin.data.util.SQLUtil; +import com.vaadin.data.util.TemporaryRowId; +import com.vaadin.data.util.connection.JDBCConnectionPool; +import com.vaadin.data.util.filter.Compare.Equal; +import com.vaadin.data.util.query.generator.DefaultSQLGenerator; +import com.vaadin.data.util.query.generator.MSSQLGenerator; +import com.vaadin.data.util.query.generator.SQLGenerator; +import com.vaadin.data.util.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() + */ + public int getCount() throws SQLException { + debug("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) + */ + 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() + */ + public boolean implementationRespectsPagingLimits() { + return true; + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.addon.sqlcontainer.query.QueryDelegate#storeRow(com.vaadin + * .addon.sqlcontainer.RowItem) + */ + 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); + debug("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) + */ + 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) + */ + 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() + */ + public void beginTransaction() throws UnsupportedOperationException, + SQLException { + if (transactionOpen && activeConnection != null) { + throw new IllegalStateException(); + } + debug("DB -> begin transaction"); + activeConnection = connectionPool.reserveConnection(); + activeConnection.setAutoCommit(false); + transactionOpen = true; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.addon.sqlcontainer.query.QueryDelegate#commit() + */ + public void commit() throws UnsupportedOperationException, SQLException { + if (transactionOpen && activeConnection != null) { + debug("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() + */ + public void rollback() throws UnsupportedOperationException, SQLException { + if (transactionOpen && activeConnection != null) { + debug("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() + */ + 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); + debug("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); + debug("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); + debug("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) { + debug("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) + */ + public boolean removeRow(RowItem row) throws UnsupportedOperationException, + SQLException { + debug("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[]) + */ + 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(); + } + } + } + + /** + * Output a debug message + * + * @param message + */ + private void debug(String message) { + if (debug) { + System.out.println(message); + } + } + + /** + * Enable or disable debug mode. + * + * @param debug + */ + public void setDebug(boolean debug) { + this.debug = debug; + } + + /** + * 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; + } + + public RowId getNewRowId() { + return newId; + } + + public RowId getOldRowId() { + return oldId; + } + } + + /** + * Adds RowIdChangeListener to this query + */ + public void addListener(RowIdChangeListener listener) { + if (rowIdChangeListeners == null) { + rowIdChangeListeners = new LinkedList<QueryDelegate.RowIdChangeListener>(); + } + rowIdChangeListeners.add(listener); + } + + /** + * Removes the given RowIdChangeListener from this query + */ + public void removeListener(RowIdChangeListener listener) { + if (rowIdChangeListeners != null) { + rowIdChangeListeners.remove(listener); + } + } +} diff --git a/src/com/vaadin/data/util/query/generator/DefaultSQLGenerator.java b/src/com/vaadin/data/util/query/generator/DefaultSQLGenerator.java new file mode 100644 index 0000000000..b904141b99 --- /dev/null +++ b/src/com/vaadin/data/util/query/generator/DefaultSQLGenerator.java @@ -0,0 +1,308 @@ +package com.vaadin.data.util.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.ColumnProperty; +import com.vaadin.data.util.RowItem; +import com.vaadin.data.util.SQLUtil; +import com.vaadin.data.util.TemporaryRowId; +import com.vaadin.data.util.query.OrderBy; +import com.vaadin.data.util.query.generator.filter.QueryBuilder; +import com.vaadin.data.util.query.generator.filter.StringDecorator; + +/** + * Generates generic SQL that is supported by HSQLDB, MySQL and PostgreSQL. + * + * @author Jonatan Kronqvist / IT Mill Ltd + */ +@SuppressWarnings("serial") +public class DefaultSQLGenerator implements SQLGenerator { + + public DefaultSQLGenerator() { + + } + + /** + * 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)); + } + + /* + * (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) + */ + 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 = new StatementHelper(); + 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) + */ + 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 = new StatementHelper(); + 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) + */ + 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 = new StatementHelper(); + 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) + */ + 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 = new StatementHelper(); + 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; + } +}
\ No newline at end of file diff --git a/src/com/vaadin/data/util/query/generator/MSSQLGenerator.java b/src/com/vaadin/data/util/query/generator/MSSQLGenerator.java new file mode 100644 index 0000000000..da98e6751b --- /dev/null +++ b/src/com/vaadin/data/util/query/generator/MSSQLGenerator.java @@ -0,0 +1,101 @@ +package com.vaadin.data.util.query.generator; + +import java.util.List; + +import com.vaadin.data.Container.Filter; +import com.vaadin.data.util.query.OrderBy; +import com.vaadin.data.util.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 = new StatementHelper(); + 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/src/com/vaadin/data/util/query/generator/OracleGenerator.java b/src/com/vaadin/data/util/query/generator/OracleGenerator.java new file mode 100644 index 0000000000..519d1822a2 --- /dev/null +++ b/src/com/vaadin/data/util/query/generator/OracleGenerator.java @@ -0,0 +1,99 @@ +package com.vaadin.data.util.query.generator; + +import java.util.List; + +import com.vaadin.data.Container.Filter; +import com.vaadin.data.util.query.OrderBy; +import com.vaadin.data.util.query.generator.filter.QueryBuilder; + +@SuppressWarnings("serial") +public class OracleGenerator extends DefaultSQLGenerator { + + public OracleGenerator() { + + } + + /** + * 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); + } + + /* + * (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 = new StatementHelper(); + 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/src/com/vaadin/data/util/query/generator/SQLGenerator.java b/src/com/vaadin/data/util/query/generator/SQLGenerator.java new file mode 100644 index 0000000000..dfdecae6f3 --- /dev/null +++ b/src/com/vaadin/data/util/query/generator/SQLGenerator.java @@ -0,0 +1,85 @@ +package com.vaadin.data.util.query.generator; + +import java.io.Serializable; +import java.util.List; + +import com.vaadin.data.Container.Filter; +import com.vaadin.data.util.RowItem; +import com.vaadin.data.util.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 / IT Mill 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/src/com/vaadin/data/util/query/generator/StatementHelper.java b/src/com/vaadin/data/util/query/generator/StatementHelper.java new file mode 100644 index 0000000000..b230f586a6 --- /dev/null +++ b/src/com/vaadin/data/util/query/generator/StatementHelper.java @@ -0,0 +1,131 @@ +package com.vaadin.data.util.query.generator;
+
+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 {
+
+ 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());
+ }
+ }
+
+ 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 {
+ throw new SQLException("Data type not supported by SQLContainer: "
+ + parameters.get(i).getClass().toString());
+ }
+ }
+}
diff --git a/src/com/vaadin/data/util/query/generator/filter/AndTranslator.java b/src/com/vaadin/data/util/query/generator/filter/AndTranslator.java new file mode 100644 index 0000000000..19f1ce5f02 --- /dev/null +++ b/src/com/vaadin/data/util/query/generator/filter/AndTranslator.java @@ -0,0 +1,18 @@ +package com.vaadin.data.util.query.generator.filter; + +import com.vaadin.data.Container.Filter; +import com.vaadin.data.util.filter.And; +import com.vaadin.data.util.query.generator.StatementHelper; + +public class AndTranslator implements FilterTranslator { + + public boolean translatesFilter(Filter filter) { + return filter instanceof And; + } + + public String getWhereStringForFilter(Filter filter, StatementHelper sh) { + return QueryBuilder.group(QueryBuilder + .getJoinedFilterString(((And) filter).getFilters(), "AND", sh)); + } + +} diff --git a/src/com/vaadin/data/util/query/generator/filter/BetweenTranslator.java b/src/com/vaadin/data/util/query/generator/filter/BetweenTranslator.java new file mode 100644 index 0000000000..0492741b0a --- /dev/null +++ b/src/com/vaadin/data/util/query/generator/filter/BetweenTranslator.java @@ -0,0 +1,21 @@ +package com.vaadin.data.util.query.generator.filter; + +import com.vaadin.data.Container.Filter; +import com.vaadin.data.util.filter.Between; +import com.vaadin.data.util.query.generator.StatementHelper; + +public class BetweenTranslator implements FilterTranslator { + + public boolean translatesFilter(Filter filter) { + return filter instanceof Between; + } + + 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/src/com/vaadin/data/util/query/generator/filter/CompareTranslator.java b/src/com/vaadin/data/util/query/generator/filter/CompareTranslator.java new file mode 100644 index 0000000000..c35baf02f5 --- /dev/null +++ b/src/com/vaadin/data/util/query/generator/filter/CompareTranslator.java @@ -0,0 +1,33 @@ +package com.vaadin.data.util.query.generator.filter; + +import com.vaadin.data.Container.Filter; +import com.vaadin.data.util.filter.Compare; +import com.vaadin.data.util.query.generator.StatementHelper; + +public class CompareTranslator implements FilterTranslator { + + public boolean translatesFilter(Filter filter) { + return filter instanceof Compare; + } + + 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/src/com/vaadin/data/util/query/generator/filter/FilterTranslator.java b/src/com/vaadin/data/util/query/generator/filter/FilterTranslator.java new file mode 100644 index 0000000000..75ac488156 --- /dev/null +++ b/src/com/vaadin/data/util/query/generator/filter/FilterTranslator.java @@ -0,0 +1,11 @@ +package com.vaadin.data.util.query.generator.filter; + +import com.vaadin.data.Container.Filter; +import com.vaadin.data.util.query.generator.StatementHelper; + +public interface FilterTranslator { + public boolean translatesFilter(Filter filter); + + public String getWhereStringForFilter(Filter filter, StatementHelper sh); + +} diff --git a/src/com/vaadin/data/util/query/generator/filter/IsNullTranslator.java b/src/com/vaadin/data/util/query/generator/filter/IsNullTranslator.java new file mode 100644 index 0000000000..fc44d65b67 --- /dev/null +++ b/src/com/vaadin/data/util/query/generator/filter/IsNullTranslator.java @@ -0,0 +1,17 @@ +package com.vaadin.data.util.query.generator.filter; + +import com.vaadin.data.Container.Filter; +import com.vaadin.data.util.filter.IsNull; +import com.vaadin.data.util.query.generator.StatementHelper; + +public class IsNullTranslator implements FilterTranslator { + + public boolean translatesFilter(Filter filter) { + return filter instanceof IsNull; + } + + public String getWhereStringForFilter(Filter filter, StatementHelper sh) { + IsNull in = (IsNull) filter; + return QueryBuilder.quote(in.getPropertyId()) + " IS NULL"; + } +} diff --git a/src/com/vaadin/data/util/query/generator/filter/LikeTranslator.java b/src/com/vaadin/data/util/query/generator/filter/LikeTranslator.java new file mode 100644 index 0000000000..e4d1d72280 --- /dev/null +++ b/src/com/vaadin/data/util/query/generator/filter/LikeTranslator.java @@ -0,0 +1,27 @@ +package com.vaadin.data.util.query.generator.filter; + +import com.vaadin.data.Container.Filter; +import com.vaadin.data.util.filter.Like; +import com.vaadin.data.util.query.generator.StatementHelper; + +public class LikeTranslator implements FilterTranslator { + + public boolean translatesFilter(Filter filter) { + return filter instanceof Like; + } + + 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/src/com/vaadin/data/util/query/generator/filter/NotTranslator.java b/src/com/vaadin/data/util/query/generator/filter/NotTranslator.java new file mode 100644 index 0000000000..1cb7321458 --- /dev/null +++ b/src/com/vaadin/data/util/query/generator/filter/NotTranslator.java @@ -0,0 +1,26 @@ +package com.vaadin.data.util.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.query.generator.StatementHelper; + +public class NotTranslator implements FilterTranslator { + + public boolean translatesFilter(Filter filter) { + return filter instanceof Not; + } + + 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/src/com/vaadin/data/util/query/generator/filter/OrTranslator.java b/src/com/vaadin/data/util/query/generator/filter/OrTranslator.java new file mode 100644 index 0000000000..1ba2db6c3a --- /dev/null +++ b/src/com/vaadin/data/util/query/generator/filter/OrTranslator.java @@ -0,0 +1,18 @@ +package com.vaadin.data.util.query.generator.filter; + +import com.vaadin.data.Container.Filter; +import com.vaadin.data.util.filter.Or; +import com.vaadin.data.util.query.generator.StatementHelper; + +public class OrTranslator implements FilterTranslator { + + public boolean translatesFilter(Filter filter) { + return filter instanceof Or; + } + + public String getWhereStringForFilter(Filter filter, StatementHelper sh) { + return QueryBuilder.group(QueryBuilder + .getJoinedFilterString(((Or) filter).getFilters(), "OR", sh)); + } + +} diff --git a/src/com/vaadin/data/util/query/generator/filter/QueryBuilder.java b/src/com/vaadin/data/util/query/generator/filter/QueryBuilder.java new file mode 100644 index 0000000000..75405d8312 --- /dev/null +++ b/src/com/vaadin/data/util/query/generator/filter/QueryBuilder.java @@ -0,0 +1,94 @@ +package com.vaadin.data.util.query.generator.filter; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import com.vaadin.data.Container.Filter; +import com.vaadin.data.util.query.generator.StatementHelper; + +public class QueryBuilder { + + 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/src/com/vaadin/data/util/query/generator/filter/SimpleStringTranslator.java b/src/com/vaadin/data/util/query/generator/filter/SimpleStringTranslator.java new file mode 100644 index 0000000000..6be678cc45 --- /dev/null +++ b/src/com/vaadin/data/util/query/generator/filter/SimpleStringTranslator.java @@ -0,0 +1,25 @@ +package com.vaadin.data.util.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.query.generator.StatementHelper; + +public class SimpleStringTranslator implements FilterTranslator { + + public boolean translatesFilter(Filter filter) { + return filter instanceof SimpleStringFilter; + } + + 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/src/com/vaadin/data/util/query/generator/filter/StringDecorator.java b/src/com/vaadin/data/util/query/generator/filter/StringDecorator.java new file mode 100644 index 0000000000..0fdd9a1f3b --- /dev/null +++ b/src/com/vaadin/data/util/query/generator/filter/StringDecorator.java @@ -0,0 +1,53 @@ +package com.vaadin.data.util.query.generator.filter; + +/** + * 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 { + + 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 + ")"; + } +} |