summaryrefslogtreecommitdiffstats
path: root/server/src/com/vaadin/data
diff options
context:
space:
mode:
authorArtur Signell <artur@vaadin.com>2012-08-13 18:34:33 +0300
committerArtur Signell <artur@vaadin.com>2012-08-13 19:18:33 +0300
commite85d933b25cc3c5cc85eb7eb4b13b950fd8e1569 (patch)
tree9ab6f13f7188cab44bbd979b1cf620f15328a03f /server/src/com/vaadin/data
parent14dd4d0b28c76eb994b181a4570f3adec53342e6 (diff)
downloadvaadin-framework-e85d933b25cc3c5cc85eb7eb4b13b950fd8e1569.tar.gz
vaadin-framework-e85d933b25cc3c5cc85eb7eb4b13b950fd8e1569.zip
Moved server files to a server src folder (#9299)
Diffstat (limited to 'server/src/com/vaadin/data')
-rw-r--r--server/src/com/vaadin/data/Buffered.java280
-rw-r--r--server/src/com/vaadin/data/BufferedValidatable.java35
-rw-r--r--server/src/com/vaadin/data/Collapsible.java68
-rw-r--r--server/src/com/vaadin/data/Container.java1105
-rw-r--r--server/src/com/vaadin/data/Item.java180
-rw-r--r--server/src/com/vaadin/data/Property.java402
-rw-r--r--server/src/com/vaadin/data/Validatable.java110
-rw-r--r--server/src/com/vaadin/data/Validator.java175
-rw-r--r--server/src/com/vaadin/data/fieldgroup/BeanFieldGroup.java157
-rw-r--r--server/src/com/vaadin/data/fieldgroup/Caption.java15
-rw-r--r--server/src/com/vaadin/data/fieldgroup/DefaultFieldGroupFieldFactory.java157
-rw-r--r--server/src/com/vaadin/data/fieldgroup/FieldGroup.java978
-rw-r--r--server/src/com/vaadin/data/fieldgroup/FieldGroupFieldFactory.java31
-rw-r--r--server/src/com/vaadin/data/fieldgroup/PropertyId.java15
-rw-r--r--server/src/com/vaadin/data/package.html49
-rw-r--r--server/src/com/vaadin/data/util/AbstractBeanContainer.java856
-rw-r--r--server/src/com/vaadin/data/util/AbstractContainer.java251
-rw-r--r--server/src/com/vaadin/data/util/AbstractInMemoryContainer.java941
-rw-r--r--server/src/com/vaadin/data/util/AbstractProperty.java226
-rw-r--r--server/src/com/vaadin/data/util/BeanContainer.java168
-rw-r--r--server/src/com/vaadin/data/util/BeanItem.java269
-rw-r--r--server/src/com/vaadin/data/util/BeanItemContainer.java241
-rw-r--r--server/src/com/vaadin/data/util/ContainerHierarchicalWrapper.java792
-rw-r--r--server/src/com/vaadin/data/util/ContainerOrderedWrapper.java644
-rw-r--r--server/src/com/vaadin/data/util/DefaultItemSorter.java210
-rw-r--r--server/src/com/vaadin/data/util/FilesystemContainer.java918
-rw-r--r--server/src/com/vaadin/data/util/HierarchicalContainer.java814
-rw-r--r--server/src/com/vaadin/data/util/HierarchicalContainerOrderedWrapper.java70
-rw-r--r--server/src/com/vaadin/data/util/IndexedContainer.java1109
-rw-r--r--server/src/com/vaadin/data/util/ItemSorter.java57
-rw-r--r--server/src/com/vaadin/data/util/ListSet.java264
-rw-r--r--server/src/com/vaadin/data/util/MethodProperty.java784
-rw-r--r--server/src/com/vaadin/data/util/MethodPropertyDescriptor.java134
-rw-r--r--server/src/com/vaadin/data/util/NestedMethodProperty.java257
-rw-r--r--server/src/com/vaadin/data/util/NestedPropertyDescriptor.java60
-rw-r--r--server/src/com/vaadin/data/util/ObjectProperty.java141
-rw-r--r--server/src/com/vaadin/data/util/PropertyFormatter.java245
-rw-r--r--server/src/com/vaadin/data/util/PropertysetItem.java340
-rw-r--r--server/src/com/vaadin/data/util/QueryContainer.java675
-rw-r--r--server/src/com/vaadin/data/util/TextFileProperty.java144
-rw-r--r--server/src/com/vaadin/data/util/TransactionalPropertyWrapper.java114
-rw-r--r--server/src/com/vaadin/data/util/VaadinPropertyDescriptor.java43
-rw-r--r--server/src/com/vaadin/data/util/converter/Converter.java159
-rw-r--r--server/src/com/vaadin/data/util/converter/ConverterFactory.java23
-rw-r--r--server/src/com/vaadin/data/util/converter/ConverterUtil.java168
-rw-r--r--server/src/com/vaadin/data/util/converter/DateToLongConverter.java72
-rw-r--r--server/src/com/vaadin/data/util/converter/DefaultConverterFactory.java101
-rw-r--r--server/src/com/vaadin/data/util/converter/ReverseConverter.java84
-rw-r--r--server/src/com/vaadin/data/util/converter/StringToBooleanConverter.java108
-rw-r--r--server/src/com/vaadin/data/util/converter/StringToDateConverter.java112
-rw-r--r--server/src/com/vaadin/data/util/converter/StringToDoubleConverter.java107
-rw-r--r--server/src/com/vaadin/data/util/converter/StringToIntegerConverter.java88
-rw-r--r--server/src/com/vaadin/data/util/converter/StringToNumberConverter.java111
-rw-r--r--server/src/com/vaadin/data/util/filter/AbstractJunctionFilter.java76
-rw-r--r--server/src/com/vaadin/data/util/filter/And.java44
-rw-r--r--server/src/com/vaadin/data/util/filter/Between.java74
-rw-r--r--server/src/com/vaadin/data/util/filter/Compare.java327
-rw-r--r--server/src/com/vaadin/data/util/filter/IsNull.java79
-rw-r--r--server/src/com/vaadin/data/util/filter/Like.java83
-rw-r--r--server/src/com/vaadin/data/util/filter/Not.java70
-rw-r--r--server/src/com/vaadin/data/util/filter/Or.java63
-rw-r--r--server/src/com/vaadin/data/util/filter/SimpleStringFilter.java152
-rw-r--r--server/src/com/vaadin/data/util/filter/UnsupportedFilterException.java35
-rw-r--r--server/src/com/vaadin/data/util/package.html18
-rw-r--r--server/src/com/vaadin/data/util/sqlcontainer/CacheFlushNotifier.java92
-rw-r--r--server/src/com/vaadin/data/util/sqlcontainer/CacheMap.java31
-rw-r--r--server/src/com/vaadin/data/util/sqlcontainer/ColumnProperty.java248
-rw-r--r--server/src/com/vaadin/data/util/sqlcontainer/OptimisticLockException.java38
-rw-r--r--server/src/com/vaadin/data/util/sqlcontainer/ReadOnlyRowId.java31
-rw-r--r--server/src/com/vaadin/data/util/sqlcontainer/Reference.java56
-rw-r--r--server/src/com/vaadin/data/util/sqlcontainer/RowId.java81
-rw-r--r--server/src/com/vaadin/data/util/sqlcontainer/RowItem.java133
-rw-r--r--server/src/com/vaadin/data/util/sqlcontainer/SQLContainer.java1716
-rw-r--r--server/src/com/vaadin/data/util/sqlcontainer/SQLUtil.java36
-rw-r--r--server/src/com/vaadin/data/util/sqlcontainer/TemporaryRowId.java32
-rw-r--r--server/src/com/vaadin/data/util/sqlcontainer/connection/J2EEConnectionPool.java72
-rw-r--r--server/src/com/vaadin/data/util/sqlcontainer/connection/JDBCConnectionPool.java41
-rw-r--r--server/src/com/vaadin/data/util/sqlcontainer/connection/SimpleJDBCConnectionPool.java168
-rw-r--r--server/src/com/vaadin/data/util/sqlcontainer/query/FreeformQuery.java507
-rw-r--r--server/src/com/vaadin/data/util/sqlcontainer/query/FreeformQueryDelegate.java118
-rw-r--r--server/src/com/vaadin/data/util/sqlcontainer/query/FreeformStatementDelegate.java57
-rw-r--r--server/src/com/vaadin/data/util/sqlcontainer/query/OrderBy.java46
-rw-r--r--server/src/com/vaadin/data/util/sqlcontainer/query/QueryDelegate.java211
-rw-r--r--server/src/com/vaadin/data/util/sqlcontainer/query/TableQuery.java715
-rw-r--r--server/src/com/vaadin/data/util/sqlcontainer/query/generator/DefaultSQLGenerator.java367
-rw-r--r--server/src/com/vaadin/data/util/sqlcontainer/query/generator/MSSQLGenerator.java101
-rw-r--r--server/src/com/vaadin/data/util/sqlcontainer/query/generator/OracleGenerator.java112
-rw-r--r--server/src/com/vaadin/data/util/sqlcontainer/query/generator/SQLGenerator.java88
-rw-r--r--server/src/com/vaadin/data/util/sqlcontainer/query/generator/StatementHelper.java163
-rw-r--r--server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/AndTranslator.java23
-rw-r--r--server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/BetweenTranslator.java25
-rw-r--r--server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/CompareTranslator.java38
-rw-r--r--server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/FilterTranslator.java16
-rw-r--r--server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/IsNullTranslator.java22
-rw-r--r--server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/LikeTranslator.java30
-rw-r--r--server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/NotTranslator.java29
-rw-r--r--server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/OrTranslator.java23
-rw-r--r--server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/QueryBuilder.java98
-rw-r--r--server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/SimpleStringTranslator.java30
-rw-r--r--server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/StringDecorator.java58
-rw-r--r--server/src/com/vaadin/data/validator/AbstractStringValidator.java42
-rw-r--r--server/src/com/vaadin/data/validator/AbstractValidator.java139
-rw-r--r--server/src/com/vaadin/data/validator/BeanValidator.java176
-rw-r--r--server/src/com/vaadin/data/validator/CompositeValidator.java259
-rw-r--r--server/src/com/vaadin/data/validator/DateRangeValidator.java51
-rw-r--r--server/src/com/vaadin/data/validator/DoubleRangeValidator.java37
-rw-r--r--server/src/com/vaadin/data/validator/DoubleValidator.java58
-rw-r--r--server/src/com/vaadin/data/validator/EmailValidator.java35
-rw-r--r--server/src/com/vaadin/data/validator/IntegerRangeValidator.java37
-rw-r--r--server/src/com/vaadin/data/validator/IntegerValidator.java58
-rw-r--r--server/src/com/vaadin/data/validator/NullValidator.java92
-rw-r--r--server/src/com/vaadin/data/validator/RangeValidator.java186
-rw-r--r--server/src/com/vaadin/data/validator/RegexpValidator.java97
-rw-r--r--server/src/com/vaadin/data/validator/StringLengthValidator.java139
-rw-r--r--server/src/com/vaadin/data/validator/package.html23
115 files changed, 23759 insertions, 0 deletions
diff --git a/server/src/com/vaadin/data/Buffered.java b/server/src/com/vaadin/data/Buffered.java
new file mode 100644
index 0000000000..1387cb965b
--- /dev/null
+++ b/server/src/com/vaadin/data/Buffered.java
@@ -0,0 +1,280 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.data;
+
+import java.io.Serializable;
+
+import com.vaadin.data.Validator.InvalidValueException;
+
+/**
+ * <p>
+ * Defines the interface to commit and discard changes to an object, supporting
+ * read-through and write-through modes.
+ * </p>
+ *
+ * <p>
+ * <i>Read-through mode</i> means that the value read from the buffered object
+ * is constantly up to date with the data source. <i>Write-through</i> mode
+ * means that all changes to the object are immediately updated to the data
+ * source.
+ * </p>
+ *
+ * <p>
+ * Since these modes are independent, their combinations may result in some
+ * behaviour that may sound surprising.
+ * </p>
+ *
+ * <p>
+ * For example, if a <code>Buffered</code> object is in read-through mode but
+ * not in write-through mode, the result is an object whose value is updated
+ * directly from the data source only if it's not locally modified. If the value
+ * is locally modified, retrieving the value from the object would result in a
+ * value that is different than the one stored in the data source, even though
+ * the object is in read-through mode.
+ * </p>
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+public interface Buffered extends Serializable {
+
+ /**
+ * Updates all changes since the previous commit to the data source. The
+ * value stored in the object will always be updated into the data source
+ * when <code>commit</code> is called.
+ *
+ * @throws SourceException
+ * if the operation fails because of an exception is thrown by
+ * the data source. The cause is included in the exception.
+ * @throws InvalidValueException
+ * if the operation fails because validation is enabled and the
+ * values do not validate
+ */
+ public void commit() throws SourceException, InvalidValueException;
+
+ /**
+ * Discards all changes since last commit. The object updates its value from
+ * the data source.
+ *
+ * @throws SourceException
+ * if the operation fails because of an exception is thrown by
+ * the data source. The cause is included in the exception.
+ */
+ public void discard() throws SourceException;
+
+ /**
+ * Tests if the object is in write-through mode. If the object is in
+ * write-through mode, all modifications to it will result in
+ * <code>commit</code> being called after the modification.
+ *
+ * @return <code>true</code> if the object is in write-through mode,
+ * <code>false</code> if it's not.
+ * @deprecated Use {@link #setBuffered(boolean)} instead. Note that
+ * setReadThrough(true), setWriteThrough(true) equals
+ * setBuffered(false)
+ */
+ @Deprecated
+ public boolean isWriteThrough();
+
+ /**
+ * Sets the object's write-through mode to the specified status. When
+ * switching the write-through mode on, the <code>commit</code> operation
+ * will be performed.
+ *
+ * @param writeThrough
+ * Boolean value to indicate if the object should be in
+ * write-through mode after the call.
+ * @throws SourceException
+ * If the operation fails because of an exception is thrown by
+ * the data source.
+ * @throws InvalidValueException
+ * If the implicit commit operation fails because of a
+ * validation error.
+ *
+ * @deprecated Use {@link #setBuffered(boolean)} instead. Note that
+ * setReadThrough(true), setWriteThrough(true) equals
+ * setBuffered(false)
+ */
+ @Deprecated
+ public void setWriteThrough(boolean writeThrough) throws SourceException,
+ InvalidValueException;
+
+ /**
+ * Tests if the object is in read-through mode. If the object is in
+ * read-through mode, retrieving its value will result in the value being
+ * first updated from the data source to the object.
+ * <p>
+ * The only exception to this rule is that when the object is not in
+ * write-through mode and it's buffer contains a modified value, the value
+ * retrieved from the object will be the locally modified value in the
+ * buffer which may differ from the value in the data source.
+ * </p>
+ *
+ * @return <code>true</code> if the object is in read-through mode,
+ * <code>false</code> if it's not.
+ * @deprecated Use {@link #isBuffered(boolean)} instead. Note that
+ * setReadThrough(true), setWriteThrough(true) equals
+ * setBuffered(false)
+ */
+ @Deprecated
+ public boolean isReadThrough();
+
+ /**
+ * Sets the object's read-through mode to the specified status. When
+ * switching read-through mode on, the object's value is updated from the
+ * data source.
+ *
+ * @param readThrough
+ * Boolean value to indicate if the object should be in
+ * read-through mode after the call.
+ *
+ * @throws SourceException
+ * If the operation fails because of an exception is thrown by
+ * the data source. The cause is included in the exception.
+ * @deprecated Use {@link #setBuffered(boolean)} instead. Note that
+ * setReadThrough(true), setWriteThrough(true) equals
+ * setBuffered(false)
+ */
+ @Deprecated
+ public void setReadThrough(boolean readThrough) throws SourceException;
+
+ /**
+ * Sets the object's buffered mode to the specified status.
+ * <p>
+ * When the object is in buffered mode, an internal buffer will be used to
+ * store changes until {@link #commit()} is called. Calling
+ * {@link #discard()} will revert the internal buffer to the value of the
+ * data source.
+ * </p>
+ * <p>
+ * This is an easier way to use {@link #setReadThrough(boolean)} and
+ * {@link #setWriteThrough(boolean)} and not as error prone. Changing
+ * buffered mode will change both the read through and write through state
+ * of the object.
+ * </p>
+ * <p>
+ * Mixing calls to {@link #setBuffered(boolean)}/{@link #isBuffered()} and
+ * {@link #setReadThrough(boolean)}/{@link #isReadThrough()} or
+ * {@link #setWriteThrough(boolean)}/{@link #isWriteThrough()} is generally
+ * a bad idea.
+ * </p>
+ *
+ * @param buffered
+ * true if buffered mode should be turned on, false otherwise
+ * @since 7.0
+ */
+ public void setBuffered(boolean buffered);
+
+ /**
+ * Checks the buffered mode of this Object.
+ * <p>
+ * This method only returns true if both read and write buffering is used.
+ * </p>
+ *
+ * @return true if buffered mode is on, false otherwise
+ * @since 7.0
+ */
+ public boolean isBuffered();
+
+ /**
+ * Tests if the value stored in the object has been modified since it was
+ * last updated from the data source.
+ *
+ * @return <code>true</code> if the value in the object has been modified
+ * since the last data source update, <code>false</code> if not.
+ */
+ public boolean isModified();
+
+ /**
+ * An exception that signals that one or more exceptions occurred while a
+ * buffered object tried to access its data source or if there is a problem
+ * in processing a data source.
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+ @SuppressWarnings("serial")
+ public class SourceException extends RuntimeException implements
+ Serializable {
+
+ /** Source class implementing the buffered interface */
+ private final Buffered source;
+
+ /** Original cause of the source exception */
+ private Throwable[] causes = {};
+
+ /**
+ * Creates a source exception that does not include a cause.
+ *
+ * @param source
+ * the source object implementing the Buffered interface.
+ */
+ public SourceException(Buffered source) {
+ this.source = source;
+ }
+
+ /**
+ * Creates a source exception from a cause exception.
+ *
+ * @param source
+ * the source object implementing the Buffered interface.
+ * @param cause
+ * the original cause for this exception.
+ */
+ public SourceException(Buffered source, Throwable cause) {
+ this.source = source;
+ causes = new Throwable[] { cause };
+ }
+
+ /**
+ * Creates a source exception from multiple causes.
+ *
+ * @param source
+ * the source object implementing the Buffered interface.
+ * @param causes
+ * the original causes for this exception.
+ */
+ public SourceException(Buffered source, Throwable[] causes) {
+ this.source = source;
+ this.causes = causes;
+ }
+
+ /**
+ * Gets the cause of the exception.
+ *
+ * @return The (first) cause for the exception, null if no cause.
+ */
+ @Override
+ public final Throwable getCause() {
+ if (causes.length == 0) {
+ return null;
+ }
+ return causes[0];
+ }
+
+ /**
+ * Gets all the causes for this exception.
+ *
+ * @return throwables that caused this exception
+ */
+ public final Throwable[] getCauses() {
+ return causes;
+ }
+
+ /**
+ * Gets a source of the exception.
+ *
+ * @return the Buffered object which generated this exception.
+ */
+ public Buffered getSource() {
+ return source;
+ }
+
+ }
+}
diff --git a/server/src/com/vaadin/data/BufferedValidatable.java b/server/src/com/vaadin/data/BufferedValidatable.java
new file mode 100644
index 0000000000..ce1d44fce6
--- /dev/null
+++ b/server/src/com/vaadin/data/BufferedValidatable.java
@@ -0,0 +1,35 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.data;
+
+import java.io.Serializable;
+
+/**
+ * <p>
+ * This interface defines the combination of <code>Validatable</code> and
+ * <code>Buffered</code> interfaces. The combination of the interfaces defines
+ * if the invalid data is committed to datasource.
+ * </p>
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+public interface BufferedValidatable extends Buffered, Validatable,
+ Serializable {
+
+ /**
+ * Tests if the invalid data is committed to datasource. The default is
+ * <code>false</code>.
+ */
+ public boolean isInvalidCommitted();
+
+ /**
+ * Sets if the invalid data should be committed to datasource. The default
+ * is <code>false</code>.
+ */
+ public void setInvalidCommitted(boolean isCommitted);
+}
diff --git a/server/src/com/vaadin/data/Collapsible.java b/server/src/com/vaadin/data/Collapsible.java
new file mode 100644
index 0000000000..06c96b7ea7
--- /dev/null
+++ b/server/src/com/vaadin/data/Collapsible.java
@@ -0,0 +1,68 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data;
+
+import com.vaadin.data.Container.Hierarchical;
+import com.vaadin.data.Container.Ordered;
+
+/**
+ * Container needed by large lazy loading hierarchies displayed e.g. in
+ * TreeTable.
+ * <p>
+ * Container of this type gets notified when a subtree is opened/closed in a
+ * component displaying its content. This allows container to lazy load subtrees
+ * and release memory when a sub-tree is no longer displayed.
+ * <p>
+ * Methods from {@link Container.Ordered} (and from {@linkContainer.Indexed} if
+ * implemented) are expected to work as in "preorder" of the currently visible
+ * hierarchy. This means for example that the return value of size method
+ * changes when subtree is collapsed/expanded. In other words items in collapsed
+ * sub trees should be "ignored" by container when the container is accessed
+ * with methods introduced in {@link Container.Ordered} or
+ * {@linkContainer.Indexed}. From the accessors point of view, items in
+ * collapsed subtrees don't exist.
+ * <p>
+ *
+ */
+public interface Collapsible extends Hierarchical, Ordered {
+
+ /**
+ * <p>
+ * Collapsing the {@link Item} indicated by <code>itemId</code> hides all
+ * children, and their respective children, from the {@link Container}.
+ * </p>
+ *
+ * <p>
+ * If called on a leaf {@link Item}, this method does nothing.
+ * </p>
+ *
+ * @param itemId
+ * the identifier of the collapsed {@link Item}
+ * @param collapsed
+ * <code>true</code> if you want to collapse the children below
+ * this {@link Item}. <code>false</code> if you want to
+ * uncollapse the children.
+ */
+ public void setCollapsed(Object itemId, boolean collapsed);
+
+ /**
+ * <p>
+ * Checks whether the {@link Item}, identified by <code>itemId</code> is
+ * collapsed or not.
+ * </p>
+ *
+ * <p>
+ * If an {@link Item} is "collapsed" its children are not included in
+ * methods used to list Items in this container.
+ * </p>
+ *
+ * @param itemId
+ * The {@link Item}'s identifier that is to be checked.
+ * @return <code>true</code> iff the {@link Item} identified by
+ * <code>itemId</code> is currently collapsed, otherwise
+ * <code>false</code>.
+ */
+ public boolean isCollapsed(Object itemId);
+
+}
diff --git a/server/src/com/vaadin/data/Container.java b/server/src/com/vaadin/data/Container.java
new file mode 100644
index 0000000000..f4c0ed9794
--- /dev/null
+++ b/server/src/com/vaadin/data/Container.java
@@ -0,0 +1,1105 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.data;
+
+import java.io.Serializable;
+import java.util.Collection;
+
+import com.vaadin.data.util.filter.SimpleStringFilter;
+import com.vaadin.data.util.filter.UnsupportedFilterException;
+
+/**
+ * <p>
+ * A specialized set of identified Items. Basically the Container is a set of
+ * {@link Item}s, but it imposes certain constraints on its contents. These
+ * constraints state the following:
+ * </p>
+ *
+ * <ul>
+ * <li>All Items in the Container must have the same number of Properties.
+ * <li>All Items in the Container must have the same Property ID's (see
+ * {@link Item#getItemPropertyIds()}).
+ * <li>All Properties in the Items corresponding to the same Property ID must
+ * have the same data type.
+ * <li>All Items within a container are uniquely identified by their non-null
+ * IDs.
+ * </ul>
+ *
+ * <p>
+ * The Container can be visualized as a representation of a relational database
+ * table. Each Item in the Container represents a row in the table, and all
+ * cells in a column (identified by a Property ID) have the same data type. Note
+ * that as with the cells in a database table, no Property in a Container may be
+ * empty, though they may contain <code>null</code> values.
+ * </p>
+ *
+ * <p>
+ * Note that though uniquely identified, the Items in a Container are not
+ * necessarily {@link Container.Ordered ordered} or {@link Container.Indexed
+ * indexed}.
+ * </p>
+ *
+ * <p>
+ * Containers can derive Item ID's from the item properties or use other,
+ * container specific or user specified identifiers.
+ * </p>
+ *
+ * <p>
+ * If a container is {@link Filterable filtered} or {@link Sortable sorted},
+ * most of the the methods of the container interface and its subinterfaces
+ * (container size, {@link #containsId(Object)}, iteration and indices etc.)
+ * relate to the filtered and sorted view, not to the full container contents.
+ * See individual method javadoc for exceptions to this (adding and removing
+ * items).
+ * </p>
+ *
+ * <p>
+ * <img src=doc-files/Container_full.gif>
+ * </p>
+ *
+ * <p>
+ * The Container interface is split to several subinterfaces so that a class can
+ * implement only the ones it needs.
+ * </p>
+ *
+ * @author Vaadin Ltd
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+public interface Container extends Serializable {
+
+ /**
+ * Gets the {@link Item} with the given Item ID from the Container. If the
+ * Container does not contain the requested Item, <code>null</code> is
+ * returned.
+ *
+ * Containers should not return Items that are filtered out.
+ *
+ * @param itemId
+ * ID of the {@link Item} to retrieve
+ * @return the {@link Item} with the given ID or <code>null</code> if the
+ * Item is not found in the Container
+ */
+ public Item getItem(Object itemId);
+
+ /**
+ * Gets the ID's of all Properties stored in the Container. The ID's cannot
+ * be modified through the returned collection.
+ *
+ * @return unmodifiable collection of Property IDs
+ */
+ public Collection<?> getContainerPropertyIds();
+
+ /**
+ * Gets the ID's of all visible (after filtering and sorting) Items stored
+ * in the Container. The ID's cannot be modified through the returned
+ * collection.
+ *
+ * If the container is {@link Ordered}, the collection returned by this
+ * method should follow that order. If the container is {@link Sortable},
+ * the items should be in the sorted order.
+ *
+ * Calling this method for large lazy containers can be an expensive
+ * operation and should be avoided when practical.
+ *
+ * @return unmodifiable collection of Item IDs
+ */
+ public Collection<?> getItemIds();
+
+ /**
+ * Gets the Property identified by the given itemId and propertyId from the
+ * Container. If the Container does not contain the item or it is filtered
+ * out, or the Container does not have the Property, <code>null</code> is
+ * returned.
+ *
+ * @param itemId
+ * ID of the visible Item which contains the Property
+ * @param propertyId
+ * ID of the Property to retrieve
+ * @return Property with the given ID or <code>null</code>
+ */
+ public Property<?> getContainerProperty(Object itemId, Object propertyId);
+
+ /**
+ * Gets the data type of all Properties identified by the given Property ID.
+ *
+ * @param propertyId
+ * ID identifying the Properties
+ * @return data type of the Properties
+ */
+ public Class<?> getType(Object propertyId);
+
+ /**
+ * Gets the number of visible Items in the Container.
+ *
+ * Filtering can hide items so that they will not be visible through the
+ * container API.
+ *
+ * @return number of Items in the Container
+ */
+ public int size();
+
+ /**
+ * Tests if the Container contains the specified Item.
+ *
+ * Filtering can hide items so that they will not be visible through the
+ * container API, and this method should respect visibility of items (i.e.
+ * only indicate visible items as being in the container) if feasible for
+ * the container.
+ *
+ * @param itemId
+ * ID the of Item to be tested
+ * @return boolean indicating if the Container holds the specified Item
+ */
+ public boolean containsId(Object itemId);
+
+ /**
+ * Creates a new Item with the given ID in the Container.
+ *
+ * <p>
+ * The new Item is returned, and it is ready to have its Properties
+ * modified. Returns <code>null</code> if the operation fails or the
+ * Container already contains a Item with the given ID.
+ * </p>
+ *
+ * <p>
+ * This functionality is optional.
+ * </p>
+ *
+ * @param itemId
+ * ID of the Item to be created
+ * @return Created new Item, or <code>null</code> in case of a failure
+ * @throws UnsupportedOperationException
+ * if adding an item with an explicit item ID is not supported
+ * by the container
+ */
+ public Item addItem(Object itemId) throws UnsupportedOperationException;
+
+ /**
+ * Creates a new Item into the Container, and assign it an automatic ID.
+ *
+ * <p>
+ * The new ID is returned, or <code>null</code> if the operation fails.
+ * After a successful call you can use the {@link #getItem(Object ItemId)
+ * <code>getItem</code>}method to fetch the Item.
+ * </p>
+ *
+ * <p>
+ * This functionality is optional.
+ * </p>
+ *
+ * @return ID of the newly created Item, or <code>null</code> in case of a
+ * failure
+ * @throws UnsupportedOperationException
+ * if adding an item without an explicit item ID is not
+ * supported by the container
+ */
+ public Object addItem() throws UnsupportedOperationException;
+
+ /**
+ * Removes the Item identified by <code>ItemId</code> from the Container.
+ *
+ * <p>
+ * Containers that support filtering should also allow removing an item that
+ * is currently filtered out.
+ * </p>
+ *
+ * <p>
+ * This functionality is optional.
+ * </p>
+ *
+ * @param itemId
+ * ID of the Item to remove
+ * @return <code>true</code> if the operation succeeded, <code>false</code>
+ * if not
+ * @throws UnsupportedOperationException
+ * if the container does not support removing individual items
+ */
+ public boolean removeItem(Object itemId)
+ throws UnsupportedOperationException;
+
+ /**
+ * Adds a new Property to all Items in the Container. The Property ID, data
+ * type and default value of the new Property are given as parameters.
+ *
+ * This functionality is optional.
+ *
+ * @param propertyId
+ * ID of the Property
+ * @param type
+ * Data type of the new Property
+ * @param defaultValue
+ * The value all created Properties are initialized to
+ * @return <code>true</code> if the operation succeeded, <code>false</code>
+ * if not
+ * @throws UnsupportedOperationException
+ * if the container does not support explicitly adding container
+ * properties
+ */
+ public boolean addContainerProperty(Object propertyId, Class<?> type,
+ Object defaultValue) throws UnsupportedOperationException;
+
+ /**
+ * Removes a Property specified by the given Property ID from the Container.
+ * Note that the Property will be removed from all Items in the Container.
+ *
+ * This functionality is optional.
+ *
+ * @param propertyId
+ * ID of the Property to remove
+ * @return <code>true</code> if the operation succeeded, <code>false</code>
+ * if not
+ * @throws UnsupportedOperationException
+ * if the container does not support removing container
+ * properties
+ */
+ public boolean removeContainerProperty(Object propertyId)
+ throws UnsupportedOperationException;
+
+ /**
+ * Removes all Items from the Container.
+ *
+ * <p>
+ * Note that Property ID and type information is preserved. This
+ * functionality is optional.
+ * </p>
+ *
+ * @return <code>true</code> if the operation succeeded, <code>false</code>
+ * if not
+ * @throws UnsupportedOperationException
+ * if the container does not support removing all items
+ */
+ public boolean removeAllItems() throws UnsupportedOperationException;
+
+ /**
+ * Interface for Container classes whose {@link Item}s can be traversed in
+ * order.
+ *
+ * <p>
+ * If the container is filtered or sorted, the traversal applies to the
+ * filtered and sorted view.
+ * </p>
+ * <p>
+ * The <code>addItemAfter()</code> methods should apply filters to the added
+ * item after inserting it, possibly hiding it immediately. If the container
+ * is being sorted, they may add items at the correct sorted position
+ * instead of the given position. See also {@link Filterable} and
+ * {@link Sortable} for more information.
+ * </p>
+ */
+ public interface Ordered extends Container {
+
+ /**
+ * Gets the ID of the Item following the Item that corresponds to
+ * <code>itemId</code>. If the given Item is the last or not found in
+ * the Container, <code>null</code> is returned.
+ *
+ * @param itemId
+ * ID of a visible Item in the Container
+ * @return ID of the next visible Item or <code>null</code>
+ */
+ public Object nextItemId(Object itemId);
+
+ /**
+ * Gets the ID of the Item preceding the Item that corresponds to
+ * <code>itemId</code>. If the given Item is the first or not found in
+ * the Container, <code>null</code> is returned.
+ *
+ * @param itemId
+ * ID of a visible Item in the Container
+ * @return ID of the previous visible Item or <code>null</code>
+ */
+ public Object prevItemId(Object itemId);
+
+ /**
+ * Gets the ID of the first Item in the Container.
+ *
+ * @return ID of the first visible Item in the Container
+ */
+ public Object firstItemId();
+
+ /**
+ * Gets the ID of the last Item in the Container..
+ *
+ * @return ID of the last visible Item in the Container
+ */
+ public Object lastItemId();
+
+ /**
+ * Tests if the Item corresponding to the given Item ID is the first
+ * Item in the Container.
+ *
+ * @param itemId
+ * ID of an Item in the Container
+ * @return <code>true</code> if the Item is first visible item in the
+ * Container, <code>false</code> if not
+ */
+ public boolean isFirstId(Object itemId);
+
+ /**
+ * Tests if the Item corresponding to the given Item ID is the last Item
+ * in the Container.
+ *
+ * @return <code>true</code> if the Item is last visible item in the
+ * Container, <code>false</code> if not
+ */
+ public boolean isLastId(Object itemId);
+
+ /**
+ * Adds a new item after the given item.
+ * <p>
+ * Adding an item after null item adds the item as first item of the
+ * ordered container.
+ * </p>
+ *
+ * @see Ordered Ordered: adding items in filtered or sorted containers
+ *
+ * @param previousItemId
+ * Id of the visible item in ordered container after which to
+ * insert the new item.
+ * @return item id the the created new item or null if the operation
+ * fails.
+ * @throws UnsupportedOperationException
+ * if the operation is not supported by the container
+ */
+ public Object addItemAfter(Object previousItemId)
+ throws UnsupportedOperationException;
+
+ /**
+ * Adds a new item after the given item.
+ * <p>
+ * Adding an item after null item adds the item as first item of the
+ * ordered container.
+ * </p>
+ *
+ * @see Ordered Ordered: adding items in filtered or sorted containers
+ *
+ * @param previousItemId
+ * Id of the visible item in ordered container after which to
+ * insert the new item.
+ * @param newItemId
+ * Id of the new item to be added.
+ * @return new item or null if the operation fails.
+ * @throws UnsupportedOperationException
+ * if the operation is not supported by the container
+ */
+ public Item addItemAfter(Object previousItemId, Object newItemId)
+ throws UnsupportedOperationException;
+
+ }
+
+ /**
+ * Interface for Container classes whose {@link Item}s can be sorted.
+ * <p>
+ * When an {@link Ordered} or {@link Indexed} container is sorted, all
+ * relevant operations of these interfaces should only use the filtered and
+ * sorted contents and the filtered indices to the container. Indices or
+ * item identifiers in the public API refer to the visible view unless
+ * otherwise stated. However, the <code>addItem*()</code> methods may add
+ * items that will be filtered out after addition or moved to another
+ * position based on sorting.
+ * </p>
+ * <p>
+ * How sorting is performed when a {@link Hierarchical} container implements
+ * {@link Sortable} is implementation specific and should be documented in
+ * the implementing class. However, the recommended approach is sorting the
+ * roots and the sets of children of each item separately.
+ * </p>
+ * <p>
+ * Depending on the container type, sorting a container may permanently
+ * change the internal order of items in the container.
+ * </p>
+ */
+ public interface Sortable extends Ordered {
+
+ /**
+ * Sort method.
+ *
+ * Sorts the container items.
+ *
+ * Sorting a container can irreversibly change the order of its items or
+ * only change the order temporarily, depending on the container.
+ *
+ * @param propertyId
+ * Array of container property IDs, whose values are used to
+ * sort the items in container as primary, secondary, ...
+ * sorting criterion. All of the item IDs must be in the
+ * collection returned by
+ * {@link #getSortableContainerPropertyIds()}
+ * @param ascending
+ * Array of sorting order flags corresponding to each
+ * property ID used in sorting. If this array is shorter than
+ * propertyId array, ascending order is assumed for items
+ * where the order is not specified. Use <code>true</code> to
+ * sort in ascending order, <code>false</code> to use
+ * descending order.
+ */
+ void sort(Object[] propertyId, boolean[] ascending);
+
+ /**
+ * Gets the container property IDs which can be used to sort the items.
+ *
+ * @return the IDs of the properties that can be used for sorting the
+ * container
+ */
+ Collection<?> getSortableContainerPropertyIds();
+
+ }
+
+ /**
+ * Interface for Container classes whose {@link Item}s can be accessed by
+ * their position in the container.
+ * <p>
+ * If the container is filtered or sorted, all indices refer to the filtered
+ * and sorted view. However, the <code>addItemAt()</code> methods may add
+ * items that will be filtered out after addition or moved to another
+ * position based on sorting.
+ * </p>
+ */
+ public interface Indexed extends Ordered {
+
+ /**
+ * Gets the index of the Item corresponding to the itemId. The following
+ * is <code>true</code> for the returned index: 0 <= index < size(), or
+ * index = -1 if there is no visible item with that id in the container.
+ *
+ * @param itemId
+ * ID of an Item in the Container
+ * @return index of the Item, or -1 if (the filtered and sorted view of)
+ * the Container does not include the Item
+ */
+ public int indexOfId(Object itemId);
+
+ /**
+ * Gets the ID of an Item by an index number.
+ *
+ * @param index
+ * Index of the requested id in (the filtered and sorted view
+ * of) the Container
+ * @return ID of the Item in the given index
+ */
+ public Object getIdByIndex(int index);
+
+ /**
+ * Adds a new item at given index (in the filtered view).
+ * <p>
+ * The indices of the item currently in the given position and all the
+ * following items are incremented.
+ * </p>
+ * <p>
+ * This method should apply filters to the added item after inserting
+ * it, possibly hiding it immediately. If the container is being sorted,
+ * the item may be added at the correct sorted position instead of the
+ * given position. See {@link Indexed}, {@link Ordered},
+ * {@link Filterable} and {@link Sortable} for more information.
+ * </p>
+ *
+ * @param index
+ * Index (in the filtered and sorted view) to add the new
+ * item.
+ * @return item id of the created item or null if the operation fails.
+ * @throws UnsupportedOperationException
+ * if the operation is not supported by the container
+ */
+ public Object addItemAt(int index) throws UnsupportedOperationException;
+
+ /**
+ * Adds a new item at given index (in the filtered view).
+ * <p>
+ * The indexes of the item currently in the given position and all the
+ * following items are incremented.
+ * </p>
+ * <p>
+ * This method should apply filters to the added item after inserting
+ * it, possibly hiding it immediately. If the container is being sorted,
+ * the item may be added at the correct sorted position instead of the
+ * given position. See {@link Indexed}, {@link Filterable} and
+ * {@link Sortable} for more information.
+ * </p>
+ *
+ * @param index
+ * Index (in the filtered and sorted view) at which to add
+ * the new item.
+ * @param newItemId
+ * Id of the new item to be added.
+ * @return new {@link Item} or null if the operation fails.
+ * @throws UnsupportedOperationException
+ * if the operation is not supported by the container
+ */
+ public Item addItemAt(int index, Object newItemId)
+ throws UnsupportedOperationException;
+
+ }
+
+ /**
+ * <p>
+ * Interface for <code>Container</code> classes whose Items can be arranged
+ * hierarchically. This means that the Items in the container belong in a
+ * tree-like structure, with the following quirks:
+ * </p>
+ *
+ * <ul>
+ * <li>The Item structure may have more than one root elements
+ * <li>The Items in the hierarchy can be declared explicitly to be able or
+ * unable to have children.
+ * </ul>
+ */
+ public interface Hierarchical extends Container {
+
+ /**
+ * Gets the IDs of all Items that are children of the specified Item.
+ * The returned collection is unmodifiable.
+ *
+ * @param itemId
+ * ID of the Item whose children the caller is interested in
+ * @return An unmodifiable {@link java.util.Collection collection}
+ * containing the IDs of all other Items that are children in
+ * the container hierarchy
+ */
+ public Collection<?> getChildren(Object itemId);
+
+ /**
+ * Gets the ID of the parent Item of the specified Item.
+ *
+ * @param itemId
+ * ID of the Item whose parent the caller wishes to find out.
+ * @return the ID of the parent Item. Will be <code>null</code> if the
+ * specified Item is a root element.
+ */
+ public Object getParent(Object itemId);
+
+ /**
+ * Gets the IDs of all Items in the container that don't have a parent.
+ * Such items are called <code>root</code> Items. The returned
+ * collection is unmodifiable.
+ *
+ * @return An unmodifiable {@link java.util.Collection collection}
+ * containing IDs of all root elements of the container
+ */
+ public Collection<?> rootItemIds();
+
+ /**
+ * <p>
+ * Sets the parent of an Item. The new parent item must exist and be
+ * able to have children. (
+ * <code>{@link #areChildrenAllowed(Object)} == true</code> ). It is
+ * also possible to detach a node from the hierarchy (and thus make it
+ * root) by setting the parent <code>null</code>.
+ * </p>
+ *
+ * <p>
+ * This operation is optional.
+ * </p>
+ *
+ * @param itemId
+ * ID of the item to be set as the child of the Item
+ * identified with <code>newParentId</code>
+ * @param newParentId
+ * ID of the Item that's to be the new parent of the Item
+ * identified with <code>itemId</code>
+ * @return <code>true</code> if the operation succeeded,
+ * <code>false</code> if not
+ */
+ public boolean setParent(Object itemId, Object newParentId)
+ throws UnsupportedOperationException;
+
+ /**
+ * Tests if the Item with given ID can have children.
+ *
+ * @param itemId
+ * ID of the Item in the container whose child capability is
+ * to be tested
+ * @return <code>true</code> if the specified Item exists in the
+ * Container and it can have children, <code>false</code> if
+ * it's not found from the container or it can't have children.
+ */
+ public boolean areChildrenAllowed(Object itemId);
+
+ /**
+ * <p>
+ * Sets the given Item's capability to have children. If the Item
+ * identified with <code>itemId</code> already has children and
+ * <code>{@link #areChildrenAllowed(Object)}</code> is false this method
+ * fails and <code>false</code> is returned.
+ * </p>
+ * <p>
+ * The children must be first explicitly removed with
+ * {@link #setParent(Object itemId, Object newParentId)}or
+ * {@link com.vaadin.data.Container#removeItem(Object itemId)}.
+ * </p>
+ *
+ * <p>
+ * This operation is optional. If it is not implemented, the method
+ * always returns <code>false</code>.
+ * </p>
+ *
+ * @param itemId
+ * ID of the Item in the container whose child capability is
+ * to be set
+ * @param areChildrenAllowed
+ * boolean value specifying if the Item can have children or
+ * not
+ * @return <code>true</code> if the operation succeeded,
+ * <code>false</code> if not
+ */
+ public boolean setChildrenAllowed(Object itemId,
+ boolean areChildrenAllowed)
+ throws UnsupportedOperationException;
+
+ /**
+ * Tests if the Item specified with <code>itemId</code> is a root Item.
+ * The hierarchical container can have more than one root and must have
+ * at least one unless it is empty. The {@link #getParent(Object itemId)}
+ * method always returns <code>null</code> for root Items.
+ *
+ * @param itemId
+ * ID of the Item whose root status is to be tested
+ * @return <code>true</code> if the specified Item is a root,
+ * <code>false</code> if not
+ */
+ public boolean isRoot(Object itemId);
+
+ /**
+ * <p>
+ * Tests if the Item specified with <code>itemId</code> has child Items
+ * or if it is a leaf. The {@link #getChildren(Object itemId)} method
+ * always returns <code>null</code> for leaf Items.
+ * </p>
+ *
+ * <p>
+ * Note that being a leaf does not imply whether or not an Item is
+ * allowed to have children.
+ * </p>
+ * .
+ *
+ * @param itemId
+ * ID of the Item to be tested
+ * @return <code>true</code> if the specified Item has children,
+ * <code>false</code> if not (is a leaf)
+ */
+ public boolean hasChildren(Object itemId);
+
+ /**
+ * <p>
+ * Removes the Item identified by <code>ItemId</code> from the
+ * Container.
+ * </p>
+ *
+ * <p>
+ * Note that this does not remove any children the item might have.
+ * </p>
+ *
+ * @param itemId
+ * ID of the Item to remove
+ * @return <code>true</code> if the operation succeeded,
+ * <code>false</code> if not
+ */
+ @Override
+ public boolean removeItem(Object itemId)
+ throws UnsupportedOperationException;
+ }
+
+ /**
+ * Interface that is implemented by containers which allow reducing their
+ * visible contents based on a set of filters. This interface has been
+ * renamed from {@link Filterable}, and implementing the new
+ * {@link Filterable} instead of or in addition to {@link SimpleFilterable}
+ * is recommended. This interface might be removed in future Vaadin
+ * versions.
+ * <p>
+ * When a set of filters are set, only items that match all the filters are
+ * included in the visible contents of the container. Still new items that
+ * do not match filters can be added to the container. Multiple filters can
+ * be added and the container remembers the state of the filters. When
+ * multiple filters are added, all filters must match for an item to be
+ * visible in the container.
+ * </p>
+ * <p>
+ * When an {@link Ordered} or {@link Indexed} container is filtered, all
+ * operations of these interfaces should only use the filtered contents and
+ * the filtered indices to the container.
+ * </p>
+ * <p>
+ * How filtering is performed when a {@link Hierarchical} container
+ * implements {@link SimpleFilterable} is implementation specific and should
+ * be documented in the implementing class.
+ * </p>
+ * <p>
+ * Adding items (if supported) to a filtered {@link Ordered} or
+ * {@link Indexed} container should insert them immediately after the
+ * indicated visible item. The unfiltered position of items added at index
+ * 0, at index {@link com.vaadin.data.Container#size()} or at an undefined
+ * position is up to the implementation.
+ * </p>
+ * <p>
+ * The functionality of SimpleFilterable can be implemented using the
+ * {@link Filterable} API and {@link SimpleStringFilter}.
+ * </p>
+ *
+ * @since 5.0 (renamed from Filterable to SimpleFilterable in 6.6)
+ */
+ public interface SimpleFilterable extends Container, Serializable {
+
+ /**
+ * Add a filter for given property.
+ *
+ * The API {@link Filterable#addContainerFilter(Filter)} is recommended
+ * instead of this method. A {@link SimpleStringFilter} can be used with
+ * the new API to implement the old string filtering functionality.
+ *
+ * The filter accepts items for which toString() of the value of the
+ * given property contains or starts with given filterString. Other
+ * items are not visible in the container when filtered.
+ *
+ * If a container has multiple filters, only items accepted by all
+ * filters are visible.
+ *
+ * @param propertyId
+ * Property for which the filter is applied to.
+ * @param filterString
+ * String that must match the value of the property
+ * @param ignoreCase
+ * Determine if the casing can be ignored when comparing
+ * strings.
+ * @param onlyMatchPrefix
+ * Only match prefixes; no other matches are included.
+ */
+ public void addContainerFilter(Object propertyId, String filterString,
+ boolean ignoreCase, boolean onlyMatchPrefix);
+
+ /**
+ * Remove all filters from all properties.
+ */
+ public void removeAllContainerFilters();
+
+ /**
+ * Remove all filters from the given property.
+ *
+ * @param propertyId
+ * for which to remove filters
+ */
+ public void removeContainerFilters(Object propertyId);
+ }
+
+ /**
+ * Filter interface for container filtering.
+ *
+ * If a filter does not support in-memory filtering,
+ * {@link #passesFilter(Item)} should throw
+ * {@link UnsupportedOperationException}.
+ *
+ * Lazy containers must be able to map filters to their internal
+ * representation (e.g. SQL or JPA 2.0 Criteria).
+ *
+ * An {@link UnsupportedFilterException} can be thrown by the container if a
+ * particular filter is not supported by the container.
+ *
+ * An {@link Filter} should implement {@link #equals(Object)} and
+ * {@link #hashCode()} correctly to avoid duplicate filter registrations
+ * etc.
+ *
+ * @see Filterable
+ *
+ * @since 6.6
+ */
+ public interface Filter extends Serializable {
+
+ /**
+ * Check if an item passes the filter (in-memory filtering).
+ *
+ * @param itemId
+ * identifier of the item being filtered; may be null when
+ * the item is being added to the container
+ * @param item
+ * the item being filtered
+ * @return true if the item is accepted by this filter
+ * @throws UnsupportedOperationException
+ * if the filter cannot be used for in-memory filtering
+ */
+ public boolean passesFilter(Object itemId, Item item)
+ throws UnsupportedOperationException;
+
+ /**
+ * Check if a change in the value of a property can affect the filtering
+ * result. May always return true, at the cost of performance.
+ *
+ * If the filter cannot determine whether it may depend on the property
+ * or not, should return true.
+ *
+ * @param propertyId
+ * @return true if the filtering result may/does change based on changes
+ * to the property identified by propertyId
+ */
+ public boolean appliesToProperty(Object propertyId);
+
+ }
+
+ /**
+ * Interface that is implemented by containers which allow reducing their
+ * visible contents based on a set of filters.
+ * <p>
+ * When a set of filters are set, only items that match all the filters are
+ * included in the visible contents of the container. Still new items that
+ * do not match filters can be added to the container. Multiple filters can
+ * be added and the container remembers the state of the filters. When
+ * multiple filters are added, all filters must match for an item to be
+ * visible in the container.
+ * </p>
+ * <p>
+ * When an {@link Ordered} or {@link Indexed} container is filtered, all
+ * operations of these interfaces should only use the filtered and sorted
+ * contents and the filtered indices to the container. Indices or item
+ * identifiers in the public API refer to the visible view unless otherwise
+ * stated. However, the <code>addItem*()</code> methods may add items that
+ * will be filtered out after addition or moved to another position based on
+ * sorting.
+ * </p>
+ * <p>
+ * How filtering is performed when a {@link Hierarchical} container
+ * implements {@link Filterable} is implementation specific and should be
+ * documented in the implementing class.
+ * </p>
+ * <p>
+ * Adding items (if supported) to a filtered {@link Ordered} or
+ * {@link Indexed} container should insert them immediately after the
+ * indicated visible item. However, the unfiltered position of items added
+ * at index 0, at index {@link com.vaadin.data.Container#size()} or at an
+ * undefined position is up to the implementation.
+ * </p>
+ *
+ * <p>
+ * This API replaces the old Filterable interface, renamed to
+ * {@link SimpleFilterable} in Vaadin 6.6.
+ * </p>
+ *
+ * @since 6.6
+ */
+ public interface Filterable extends Container, Serializable {
+ /**
+ * Adds a filter for the container.
+ *
+ * If a container has multiple filters, only items accepted by all
+ * filters are visible.
+ *
+ * @throws UnsupportedFilterException
+ * if the filter is not supported by the container
+ */
+ public void addContainerFilter(Filter filter)
+ throws UnsupportedFilterException;
+
+ /**
+ * Removes a filter from the container.
+ *
+ * This requires that the equals() method considers the filters as
+ * equivalent (same instance or properly implemented equals() method).
+ */
+ public void removeContainerFilter(Filter filter);
+
+ /**
+ * Remove all active filters from the container.
+ */
+ public void removeAllContainerFilters();
+
+ }
+
+ /**
+ * Interface implemented by viewer classes capable of using a Container as a
+ * data source.
+ */
+ public interface Viewer extends Serializable {
+
+ /**
+ * Sets the Container that serves as the data source of the viewer.
+ *
+ * @param newDataSource
+ * The new data source Item
+ */
+ public void setContainerDataSource(Container newDataSource);
+
+ /**
+ * Gets the Container serving as the data source of the viewer.
+ *
+ * @return data source Container
+ */
+ public Container getContainerDataSource();
+
+ }
+
+ /**
+ * <p>
+ * Interface implemented by the editor classes supporting editing the
+ * Container. Implementing this interface means that the Container serving
+ * as the data source of the editor can be modified through it.
+ * </p>
+ * <p>
+ * Note that not implementing the <code>Container.Editor</code> interface
+ * does not restrict the class from editing the Container contents
+ * internally.
+ * </p>
+ */
+ public interface Editor extends Container.Viewer, Serializable {
+
+ }
+
+ /* Contents change event */
+
+ /**
+ * An <code>Event</code> object specifying the Container whose Item set has
+ * changed (items added, removed or reordered).
+ *
+ * A simple property value change is not an item set change.
+ */
+ public interface ItemSetChangeEvent extends Serializable {
+
+ /**
+ * Gets the Property where the event occurred.
+ *
+ * @return source of the event
+ */
+ public Container getContainer();
+ }
+
+ /**
+ * Container Item set change listener interface.
+ *
+ * An item set change refers to addition, removal or reordering of items in
+ * the container. A simple property value change is not an item set change.
+ */
+ public interface ItemSetChangeListener extends Serializable {
+
+ /**
+ * Lets the listener know a Containers visible (filtered and/or sorted,
+ * if applicable) Item set has changed.
+ *
+ * @param event
+ * change event text
+ */
+ public void containerItemSetChange(Container.ItemSetChangeEvent event);
+ }
+
+ /**
+ * The interface for adding and removing <code>ItemSetChangeEvent</code>
+ * listeners. By implementing this interface a class explicitly announces
+ * that it will generate a <code>ItemSetChangeEvent</code> when its contents
+ * are modified.
+ *
+ * An item set change refers to addition, removal or reordering of items in
+ * the container. A simple property value change is not an item set change.
+ *
+ * <p>
+ * Note: The general Java convention is not to explicitly declare that a
+ * class generates events, but to directly define the
+ * <code>addListener</code> and <code>removeListener</code> methods. That
+ * way the caller of these methods has no real way of finding out if the
+ * class really will send the events, or if it just defines the methods to
+ * be able to implement an interface.
+ * </p>
+ */
+ public interface ItemSetChangeNotifier extends Serializable {
+
+ /**
+ * Adds an Item set change listener for the object.
+ *
+ * @param listener
+ * listener to be added
+ */
+ public void addListener(Container.ItemSetChangeListener listener);
+
+ /**
+ * Removes the Item set change listener from the object.
+ *
+ * @param listener
+ * listener to be removed
+ */
+ public void removeListener(Container.ItemSetChangeListener listener);
+ }
+
+ /* Property set change event */
+
+ /**
+ * An <code>Event</code> object specifying the Container whose Property set
+ * has changed.
+ *
+ * A property set change means the addition, removal or other structural
+ * changes to the properties of a container. Changes concerning the set of
+ * items in the container and their property values are not property set
+ * changes.
+ */
+ public interface PropertySetChangeEvent extends Serializable {
+
+ /**
+ * Retrieves the Container whose contents have been modified.
+ *
+ * @return Source Container of the event.
+ */
+ public Container getContainer();
+ }
+
+ /**
+ * The listener interface for receiving <code>PropertySetChangeEvent</code>
+ * objects.
+ *
+ * A property set change means the addition, removal or other structural
+ * change of the properties (supported property IDs) of a container. Changes
+ * concerning the set of items in the container and their property values
+ * are not property set changes.
+ */
+ public interface PropertySetChangeListener extends Serializable {
+
+ /**
+ * Notifies this listener that the set of property IDs supported by the
+ * Container has changed.
+ *
+ * @param event
+ * Change event.
+ */
+ public void containerPropertySetChange(
+ Container.PropertySetChangeEvent event);
+ }
+
+ /**
+ * <p>
+ * The interface for adding and removing <code>PropertySetChangeEvent</code>
+ * listeners. By implementing this interface a class explicitly announces
+ * that it will generate a <code>PropertySetChangeEvent</code> when the set
+ * of property IDs supported by the container is modified.
+ * </p>
+ *
+ * <p>
+ * A property set change means the addition, removal or other structural
+ * changes to the properties of a container. Changes concerning the set of
+ * items in the container and their property values are not property set
+ * changes.
+ * </p>
+ *
+ * <p>
+ * Note that the general Java convention is not to explicitly declare that a
+ * class generates events, but to directly define the
+ * <code>addListener</code> and <code>removeListener</code> methods. That
+ * way the caller of these methods has no real way of finding out if the
+ * class really will send the events, or if it just defines the methods to
+ * be able to implement an interface.
+ * </p>
+ */
+ public interface PropertySetChangeNotifier extends Serializable {
+
+ /**
+ * Registers a new Property set change listener for this Container.
+ *
+ * @param listener
+ * The new Listener to be registered
+ */
+ public void addListener(Container.PropertySetChangeListener listener);
+
+ /**
+ * Removes a previously registered Property set change listener.
+ *
+ * @param listener
+ * Listener to be removed
+ */
+ public void removeListener(Container.PropertySetChangeListener listener);
+ }
+}
diff --git a/server/src/com/vaadin/data/Item.java b/server/src/com/vaadin/data/Item.java
new file mode 100644
index 0000000000..98b95aecff
--- /dev/null
+++ b/server/src/com/vaadin/data/Item.java
@@ -0,0 +1,180 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.data;
+
+import java.io.Serializable;
+import java.util.Collection;
+
+/**
+ * <p>
+ * Provides a mechanism for handling a set of Properties, each associated to a
+ * locally unique non-null identifier. The interface is split into subinterfaces
+ * to enable a class to implement only the functionalities it needs.
+ * </p>
+ *
+ * @author Vaadin Ltd
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+public interface Item extends Serializable {
+
+ /**
+ * Gets the Property corresponding to the given Property ID stored in the
+ * Item. If the Item does not contain the Property, <code>null</code> is
+ * returned.
+ *
+ * @param id
+ * identifier of the Property to get
+ * @return the Property with the given ID or <code>null</code>
+ */
+ public Property<?> getItemProperty(Object id);
+
+ /**
+ * Gets the collection of IDs of all Properties stored in the Item.
+ *
+ * @return unmodifiable collection containing IDs of the Properties stored
+ * the Item
+ */
+ public Collection<?> getItemPropertyIds();
+
+ /**
+ * Tries to add a new Property into the Item.
+ *
+ * <p>
+ * This functionality is optional.
+ * </p>
+ *
+ * @param id
+ * ID of the new Property
+ * @param property
+ * the Property to be added and associated with the id
+ * @return <code>true</code> if the operation succeeded, <code>false</code>
+ * if not
+ * @throws UnsupportedOperationException
+ * if the operation is not supported.
+ */
+ public boolean addItemProperty(Object id, Property property)
+ throws UnsupportedOperationException;
+
+ /**
+ * Removes the Property identified by ID from the Item.
+ *
+ * <p>
+ * This functionality is optional.
+ * </p>
+ *
+ * @param id
+ * ID of the Property to be removed
+ * @return <code>true</code> if the operation succeeded
+ * @throws UnsupportedOperationException
+ * if the operation is not supported. <code>false</code> if not
+ */
+ public boolean removeItemProperty(Object id)
+ throws UnsupportedOperationException;
+
+ /**
+ * Interface implemented by viewer classes capable of using an Item as a
+ * data source.
+ */
+ public interface Viewer extends Serializable {
+
+ /**
+ * Sets the Item that serves as the data source of the viewer.
+ *
+ * @param newDataSource
+ * The new data source Item
+ */
+ public void setItemDataSource(Item newDataSource);
+
+ /**
+ * Gets the Item serving as the data source of the viewer.
+ *
+ * @return data source Item
+ */
+ public Item getItemDataSource();
+ }
+
+ /**
+ * Interface implemented by the <code>Editor</code> classes capable of
+ * editing the Item. Implementing this interface means that the Item serving
+ * as the data source of the editor can be modified through it.
+ * <p>
+ * Note : Not implementing the <code>Item.Editor</code> interface does not
+ * restrict the class from editing the contents of an internally.
+ * </p>
+ */
+ public interface Editor extends Item.Viewer, Serializable {
+
+ }
+
+ /* Property set change event */
+
+ /**
+ * An <code>Event</code> object specifying the Item whose contents has been
+ * changed through the <code>Property</code> interface.
+ * <p>
+ * Note: The values stored in the Properties may change without triggering
+ * this event.
+ * </p>
+ */
+ public interface PropertySetChangeEvent extends Serializable {
+
+ /**
+ * Retrieves the Item whose contents has been modified.
+ *
+ * @return source Item of the event
+ */
+ public Item getItem();
+ }
+
+ /**
+ * The listener interface for receiving <code>PropertySetChangeEvent</code>
+ * objects.
+ */
+ public interface PropertySetChangeListener extends Serializable {
+
+ /**
+ * Notifies this listener that the Item's property set has changed.
+ *
+ * @param event
+ * Property set change event object
+ */
+ public void itemPropertySetChange(Item.PropertySetChangeEvent event);
+ }
+
+ /**
+ * The interface for adding and removing <code>PropertySetChangeEvent</code>
+ * listeners. By implementing this interface a class explicitly announces
+ * that it will generate a <code>PropertySetChangeEvent</code> when its
+ * Property set is modified.
+ * <p>
+ * Note : The general Java convention is not to explicitly declare that a
+ * class generates events, but to directly define the
+ * <code>addListener</code> and <code>removeListener</code> methods. That
+ * way the caller of these methods has no real way of finding out if the
+ * class really will send the events, or if it just defines the methods to
+ * be able to implement an interface.
+ * </p>
+ */
+ public interface PropertySetChangeNotifier extends Serializable {
+
+ /**
+ * Registers a new property set change listener for this Item.
+ *
+ * @param listener
+ * The new Listener to be registered.
+ */
+ public void addListener(Item.PropertySetChangeListener listener);
+
+ /**
+ * Removes a previously registered property set change listener.
+ *
+ * @param listener
+ * Listener to be removed.
+ */
+ public void removeListener(Item.PropertySetChangeListener listener);
+ }
+}
diff --git a/server/src/com/vaadin/data/Property.java b/server/src/com/vaadin/data/Property.java
new file mode 100644
index 0000000000..9fab642381
--- /dev/null
+++ b/server/src/com/vaadin/data/Property.java
@@ -0,0 +1,402 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.data;
+
+import java.io.Serializable;
+
+/**
+ * <p>
+ * The <code>Property</code> is a simple data object that contains one typed
+ * value. This interface contains methods to inspect and modify the stored value
+ * and its type, and the object's read-only state.
+ * </p>
+ *
+ * <p>
+ * The <code>Property</code> also defines the events
+ * <code>ReadOnlyStatusChangeEvent</code> and <code>ValueChangeEvent</code>, and
+ * the associated <code>listener</code> and <code>notifier</code> interfaces.
+ * </p>
+ *
+ * <p>
+ * The <code>Property.Viewer</code> interface should be used to attach the
+ * Property to an external data source. This way the value in the data source
+ * can be inspected using the <code>Property</code> interface.
+ * </p>
+ *
+ * <p>
+ * The <code>Property.editor</code> interface should be implemented if the value
+ * needs to be changed through the implementing class.
+ * </p>
+ *
+ * @param T
+ * type of values of the property
+ *
+ * @author Vaadin Ltd
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+public interface Property<T> extends Serializable {
+
+ /**
+ * Gets the value stored in the Property. The returned object is compatible
+ * with the class returned by getType().
+ *
+ * @return the value stored in the Property
+ */
+ public T getValue();
+
+ /**
+ * Sets the value of the Property.
+ * <p>
+ * Implementing this functionality is optional. If the functionality is
+ * missing, one should declare the Property to be in read-only mode and
+ * throw <code>Property.ReadOnlyException</code> in this function.
+ * </p>
+ *
+ * Note : Since Vaadin 7.0, setting the value of a non-String property as a
+ * String is no longer supported.
+ *
+ * @param newValue
+ * New value of the Property. This should be assignable to the
+ * type returned by getType
+ *
+ * @throws Property.ReadOnlyException
+ * if the object is in read-only mode
+ */
+ public void setValue(Object newValue) throws Property.ReadOnlyException;
+
+ /**
+ * Returns the type of the Property. The methods <code>getValue</code> and
+ * <code>setValue</code> must be compatible with this type: one must be able
+ * to safely cast the value returned from <code>getValue</code> to the given
+ * type and pass any variable assignable to this type as an argument to
+ * <code>setValue</code>.
+ *
+ * @return type of the Property
+ */
+ public Class<? extends T> getType();
+
+ /**
+ * Tests if the Property is in read-only mode. In read-only mode calls to
+ * the method <code>setValue</code> will throw
+ * <code>ReadOnlyException</code> and will not modify the value of the
+ * Property.
+ *
+ * @return <code>true</code> if the Property is in read-only mode,
+ * <code>false</code> if it's not
+ */
+ public boolean isReadOnly();
+
+ /**
+ * Sets the Property's read-only mode to the specified status.
+ *
+ * This functionality is optional, but all properties must implement the
+ * <code>isReadOnly</code> mode query correctly.
+ *
+ * @param newStatus
+ * new read-only status of the Property
+ */
+ public void setReadOnly(boolean newStatus);
+
+ /**
+ * A Property that is capable of handle a transaction that can end in commit
+ * or rollback.
+ *
+ * Note that this does not refer to e.g. database transactions but rather
+ * two-phase commit that allows resetting old field values on a form etc. if
+ * the commit of one of the properties fails after others have already been
+ * committed. If
+ *
+ * @param <T>
+ * The type of the property
+ * @author Vaadin Ltd
+ * @version @version@
+ * @since 7.0
+ */
+ public interface Transactional<T> extends Property<T> {
+
+ /**
+ * Starts a transaction.
+ *
+ * <p>
+ * If the value is set during a transaction the value must not replace
+ * the original value until {@link #commit()} is called. Still,
+ * {@link #getValue()} must return the current value set in the
+ * transaction. Calling {@link #rollback()} while in a transaction must
+ * rollback the value to what it was before the transaction started.
+ * </p>
+ * <p>
+ * {@link ValueChangeEvent}s must not be emitted for internal value
+ * changes during a transaction. If the value changes as a result of
+ * {@link #commit()}, a {@link ValueChangeEvent} should be emitted.
+ * </p>
+ */
+ public void startTransaction();
+
+ /**
+ * Commits and ends the transaction that is in progress.
+ * <p>
+ * If the value is changed as a result of this operation, a
+ * {@link ValueChangeEvent} is emitted if such are supported.
+ * <p>
+ * This method has no effect if there is no transaction is in progress.
+ * <p>
+ * This method must never throw an exception.
+ */
+ public void commit();
+
+ /**
+ * Aborts and rolls back the transaction that is in progress.
+ * <p>
+ * The value is reset to the value before the transaction started. No
+ * {@link ValueChangeEvent} is emitted as a result of this.
+ * <p>
+ * This method has no effect if there is no transaction is in progress.
+ * <p>
+ * This method must never throw an exception.
+ */
+ public void rollback();
+ }
+
+ /**
+ * <code>Exception</code> object that signals that a requested Property
+ * modification failed because it's in read-only mode.
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+ @SuppressWarnings("serial")
+ public class ReadOnlyException extends RuntimeException {
+
+ /**
+ * Constructs a new <code>ReadOnlyException</code> without a detail
+ * message.
+ */
+ public ReadOnlyException() {
+ }
+
+ /**
+ * Constructs a new <code>ReadOnlyException</code> with the specified
+ * detail message.
+ *
+ * @param msg
+ * the detail message
+ */
+ public ReadOnlyException(String msg) {
+ super(msg);
+ }
+ }
+
+ /**
+ * Interface implemented by the viewer classes capable of using a Property
+ * as a data source.
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+ public interface Viewer extends Serializable {
+
+ /**
+ * Sets the Property that serves as the data source of the viewer.
+ *
+ * @param newDataSource
+ * the new data source Property
+ */
+ public void setPropertyDataSource(Property newDataSource);
+
+ /**
+ * Gets the Property serving as the data source of the viewer.
+ *
+ * @return the Property serving as the viewers data source
+ */
+ public Property getPropertyDataSource();
+ }
+
+ /**
+ * Interface implemented by the editor classes capable of editing the
+ * Property.
+ * <p>
+ * Implementing this interface means that the Property serving as the data
+ * source of the editor can be modified through the editor. It does not
+ * restrict the editor from editing the Property internally, though if the
+ * Property is in a read-only mode, attempts to modify it will result in the
+ * <code>ReadOnlyException</code> being thrown.
+ * </p>
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+ public interface Editor extends Property.Viewer, Serializable {
+
+ }
+
+ /* Value change event */
+
+ /**
+ * An <code>Event</code> object specifying the Property whose value has been
+ * changed.
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+ public interface ValueChangeEvent extends Serializable {
+
+ /**
+ * Retrieves the Property that has been modified.
+ *
+ * @return source Property of the event
+ */
+ public Property getProperty();
+ }
+
+ /**
+ * The <code>listener</code> interface for receiving
+ * <code>ValueChangeEvent</code> objects.
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+ public interface ValueChangeListener extends Serializable {
+
+ /**
+ * Notifies this listener that the Property's value has changed.
+ *
+ * @param event
+ * value change event object
+ */
+ public void valueChange(Property.ValueChangeEvent event);
+ }
+
+ /**
+ * The interface for adding and removing <code>ValueChangeEvent</code>
+ * listeners. If a Property wishes to allow other objects to receive
+ * <code>ValueChangeEvent</code> generated by it, it must implement this
+ * interface.
+ * <p>
+ * Note : The general Java convention is not to explicitly declare that a
+ * class generates events, but to directly define the
+ * <code>addListener</code> and <code>removeListener</code> methods. That
+ * way the caller of these methods has no real way of finding out if the
+ * class really will send the events, or if it just defines the methods to
+ * be able to implement an interface.
+ * </p>
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+ public interface ValueChangeNotifier extends Serializable {
+
+ /**
+ * Registers a new value change listener for this Property.
+ *
+ * @param listener
+ * the new Listener to be registered
+ */
+ public void addListener(Property.ValueChangeListener listener);
+
+ /**
+ * Removes a previously registered value change listener.
+ *
+ * @param listener
+ * listener to be removed
+ */
+ public void removeListener(Property.ValueChangeListener listener);
+ }
+
+ /* ReadOnly Status change event */
+
+ /**
+ * An <code>Event</code> object specifying the Property whose read-only
+ * status has been changed.
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+ public interface ReadOnlyStatusChangeEvent extends Serializable {
+
+ /**
+ * Property whose read-only state has changed.
+ *
+ * @return source Property of the event.
+ */
+ public Property getProperty();
+ }
+
+ /**
+ * The listener interface for receiving
+ * <code>ReadOnlyStatusChangeEvent</code> objects.
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+ public interface ReadOnlyStatusChangeListener extends Serializable {
+
+ /**
+ * Notifies this listener that a Property's read-only status has
+ * changed.
+ *
+ * @param event
+ * Read-only status change event object
+ */
+ public void readOnlyStatusChange(
+ Property.ReadOnlyStatusChangeEvent event);
+ }
+
+ /**
+ * The interface for adding and removing
+ * <code>ReadOnlyStatusChangeEvent</code> listeners. If a Property wishes to
+ * allow other objects to receive <code>ReadOnlyStatusChangeEvent</code>
+ * generated by it, it must implement this interface.
+ * <p>
+ * Note : The general Java convention is not to explicitly declare that a
+ * class generates events, but to directly define the
+ * <code>addListener</code> and <code>removeListener</code> methods. That
+ * way the caller of these methods has no real way of finding out if the
+ * class really will send the events, or if it just defines the methods to
+ * be able to implement an interface.
+ * </p>
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+ public interface ReadOnlyStatusChangeNotifier extends Serializable {
+
+ /**
+ * Registers a new read-only status change listener for this Property.
+ *
+ * @param listener
+ * the new Listener to be registered
+ */
+ public void addListener(Property.ReadOnlyStatusChangeListener listener);
+
+ /**
+ * Removes a previously registered read-only status change listener.
+ *
+ * @param listener
+ * listener to be removed
+ */
+ public void removeListener(
+ Property.ReadOnlyStatusChangeListener listener);
+ }
+}
diff --git a/server/src/com/vaadin/data/Validatable.java b/server/src/com/vaadin/data/Validatable.java
new file mode 100644
index 0000000000..4a7a0fda10
--- /dev/null
+++ b/server/src/com/vaadin/data/Validatable.java
@@ -0,0 +1,110 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.data;
+
+import java.io.Serializable;
+import java.util.Collection;
+
+/**
+ * <p>
+ * Interface for validatable objects. Defines methods to verify if the object's
+ * value is valid or not, and to add, remove and list registered validators of
+ * the object.
+ * </p>
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ * @see com.vaadin.data.Validator
+ */
+public interface Validatable extends Serializable {
+
+ /**
+ * <p>
+ * Adds a new validator for this object. The validator's
+ * {@link Validator#validate(Object)} method is activated every time the
+ * object's value needs to be verified, that is, when the {@link #isValid()}
+ * method is called. This usually happens when the object's value changes.
+ * </p>
+ *
+ * @param validator
+ * the new validator
+ */
+ void addValidator(Validator validator);
+
+ /**
+ * <p>
+ * Removes a previously registered validator from the object. The specified
+ * validator is removed from the object and its <code>validate</code> method
+ * is no longer called in {@link #isValid()}.
+ * </p>
+ *
+ * @param validator
+ * the validator to remove
+ */
+ void removeValidator(Validator validator);
+
+ /**
+ * <p>
+ * Lists all validators currently registered for the object. If no
+ * validators are registered, returns <code>null</code>.
+ * </p>
+ *
+ * @return collection of validators or <code>null</code>
+ */
+ public Collection<Validator> getValidators();
+
+ /**
+ * <p>
+ * Tests the current value of the object against all registered validators.
+ * The registered validators are iterated and for each the
+ * {@link Validator#validate(Object)} method is called. If any validator
+ * throws the {@link Validator.InvalidValueException} this method returns
+ * <code>false</code>.
+ * </p>
+ *
+ * @return <code>true</code> if the registered validators concur that the
+ * value is valid, <code>false</code> otherwise
+ */
+ public boolean isValid();
+
+ /**
+ * <p>
+ * Checks the validity of the validatable. If the validatable is valid this
+ * method should do nothing, and if it's not valid, it should throw
+ * <code>Validator.InvalidValueException</code>
+ * </p>
+ *
+ * @throws Validator.InvalidValueException
+ * if the value is not valid
+ */
+ public void validate() throws Validator.InvalidValueException;
+
+ /**
+ * <p>
+ * Checks the validabtable object accept invalid values.The default value is
+ * <code>true</code>.
+ * </p>
+ *
+ */
+ public boolean isInvalidAllowed();
+
+ /**
+ * <p>
+ * Should the validabtable object accept invalid values. Supporting this
+ * configuration possibility is optional. By default invalid values are
+ * allowed.
+ * </p>
+ *
+ * @param invalidValueAllowed
+ *
+ * @throws UnsupportedOperationException
+ * if the setInvalidAllowed is not supported.
+ */
+ public void setInvalidAllowed(boolean invalidValueAllowed)
+ throws UnsupportedOperationException;
+
+}
diff --git a/server/src/com/vaadin/data/Validator.java b/server/src/com/vaadin/data/Validator.java
new file mode 100644
index 0000000000..768a02babe
--- /dev/null
+++ b/server/src/com/vaadin/data/Validator.java
@@ -0,0 +1,175 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.data;
+
+import java.io.Serializable;
+
+import com.vaadin.terminal.gwt.server.AbstractApplicationServlet;
+
+/**
+ * Interface that implements a method for validating if an {@link Object} is
+ * valid or not.
+ * <p>
+ * Implementors of this class can be added to any
+ * {@link com.vaadin.data.Validatable Validatable} implementor to verify its
+ * value.
+ * </p>
+ * <p>
+ * {@link #validate(Object)} can be used to check if a value is valid. An
+ * {@link InvalidValueException} with an appropriate validation error message is
+ * thrown if the value is not valid.
+ * </p>
+ * <p>
+ * Validators must not have any side effects.
+ * </p>
+ * <p>
+ * Since Vaadin 7, the method isValid(Object) does not exist in the interface -
+ * {@link #validate(Object)} should be used instead, and the exception caught
+ * where applicable. Concrete classes implementing {@link Validator} can still
+ * internally implement and use isValid(Object) for convenience or to ease
+ * migration from earlier Vaadin versions.
+ * </p>
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+public interface Validator extends Serializable {
+
+ /**
+ * Checks the given value against this validator. If the value is valid the
+ * method does nothing. If the value is invalid, an
+ * {@link InvalidValueException} is thrown.
+ *
+ * @param value
+ * the value to check
+ * @throws Validator.InvalidValueException
+ * if the value is invalid
+ */
+ public void validate(Object value) throws Validator.InvalidValueException;
+
+ /**
+ * Exception that is thrown by a {@link Validator} when a value is invalid.
+ *
+ * <p>
+ * The default implementation of InvalidValueException does not support HTML
+ * in error messages. To enable HTML support, override
+ * {@link #getHtmlMessage()} and use the subclass in validators.
+ * </p>
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+ @SuppressWarnings("serial")
+ public class InvalidValueException extends RuntimeException {
+
+ /**
+ * Array of one or more validation errors that are causing this
+ * validation error.
+ */
+ private InvalidValueException[] causes = null;
+
+ /**
+ * Constructs a new {@code InvalidValueException} with the specified
+ * message.
+ *
+ * @param message
+ * The detail message of the problem.
+ */
+ public InvalidValueException(String message) {
+ this(message, new InvalidValueException[] {});
+ }
+
+ /**
+ * Constructs a new {@code InvalidValueException} with a set of causing
+ * validation exceptions. The causing validation exceptions are included
+ * when the exception is painted to the client.
+ *
+ * @param message
+ * The detail message of the problem.
+ * @param causes
+ * One or more {@code InvalidValueException}s that caused
+ * this exception.
+ */
+ public InvalidValueException(String message,
+ InvalidValueException[] causes) {
+ super(message);
+ if (causes == null) {
+ throw new NullPointerException(
+ "Possible causes array must not be null");
+ }
+
+ this.causes = causes;
+ }
+
+ /**
+ * Check if the error message should be hidden.
+ *
+ * An empty (null or "") message is invisible unless it contains nested
+ * exceptions that are visible.
+ *
+ * @return true if the error message should be hidden, false otherwise
+ */
+ public boolean isInvisible() {
+ String msg = getMessage();
+ if (msg != null && msg.length() > 0) {
+ return false;
+ }
+ if (causes != null) {
+ for (int i = 0; i < causes.length; i++) {
+ if (!causes[i].isInvisible()) {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Returns the message of the error in HTML.
+ *
+ * Note that this API may change in future versions.
+ */
+ public String getHtmlMessage() {
+ return AbstractApplicationServlet
+ .safeEscapeForHtml(getLocalizedMessage());
+ }
+
+ /**
+ * Returns the {@code InvalidValueExceptions} that caused this
+ * exception.
+ *
+ * @return An array containing the {@code InvalidValueExceptions} that
+ * caused this exception. Returns an empty array if this
+ * exception was not caused by other exceptions.
+ */
+ public InvalidValueException[] getCauses() {
+ return causes;
+ }
+
+ }
+
+ /**
+ * A specific type of {@link InvalidValueException} that indicates that
+ * validation failed because the value was empty. What empty means is up to
+ * the thrower.
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 5.3.0
+ */
+ @SuppressWarnings("serial")
+ public class EmptyValueException extends Validator.InvalidValueException {
+
+ public EmptyValueException(String message) {
+ super(message);
+ }
+
+ }
+}
diff --git a/server/src/com/vaadin/data/fieldgroup/BeanFieldGroup.java b/server/src/com/vaadin/data/fieldgroup/BeanFieldGroup.java
new file mode 100644
index 0000000000..b8efa5b1e4
--- /dev/null
+++ b/server/src/com/vaadin/data/fieldgroup/BeanFieldGroup.java
@@ -0,0 +1,157 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.fieldgroup;
+
+import java.lang.reflect.Method;
+
+import com.vaadin.data.Item;
+import com.vaadin.data.util.BeanItem;
+import com.vaadin.data.validator.BeanValidator;
+import com.vaadin.ui.Field;
+
+public class BeanFieldGroup<T> extends FieldGroup {
+
+ private Class<T> beanType;
+
+ private static Boolean beanValidationImplementationAvailable = null;
+
+ public BeanFieldGroup(Class<T> beanType) {
+ this.beanType = beanType;
+ }
+
+ @Override
+ protected Class<?> getPropertyType(Object propertyId) {
+ if (getItemDataSource() != null) {
+ return super.getPropertyType(propertyId);
+ } else {
+ // Data source not set so we need to figure out the type manually
+ /*
+ * toString should never really be needed as propertyId should be of
+ * form "fieldName" or "fieldName.subField[.subField2]" but the
+ * method declaration comes from parent.
+ */
+ java.lang.reflect.Field f;
+ try {
+ f = getField(beanType, propertyId.toString());
+ return f.getType();
+ } catch (SecurityException e) {
+ throw new BindException("Cannot determine type of propertyId '"
+ + propertyId + "'.", e);
+ } catch (NoSuchFieldException e) {
+ throw new BindException("Cannot determine type of propertyId '"
+ + propertyId + "'. The propertyId was not found in "
+ + beanType.getName(), e);
+ }
+ }
+ }
+
+ private static java.lang.reflect.Field getField(Class<?> cls,
+ String propertyId) throws SecurityException, NoSuchFieldException {
+ if (propertyId.contains(".")) {
+ String[] parts = propertyId.split("\\.", 2);
+ // Get the type of the field in the "cls" class
+ java.lang.reflect.Field field1 = getField(cls, parts[0]);
+ // Find the rest from the sub type
+ return getField(field1.getType(), parts[1]);
+ } else {
+ try {
+ // Try to find the field directly in the given class
+ java.lang.reflect.Field field1 = cls
+ .getDeclaredField(propertyId);
+ return field1;
+ } catch (NoSuchFieldError e) {
+ // Try super classes until we reach Object
+ Class<?> superClass = cls.getSuperclass();
+ if (superClass != Object.class) {
+ return getField(superClass, propertyId);
+ } else {
+ throw e;
+ }
+ }
+ }
+ }
+
+ /**
+ * Helper method for setting the data source directly using a bean. This
+ * method wraps the bean in a {@link BeanItem} and calls
+ * {@link #setItemDataSource(Item)}.
+ *
+ * @param bean
+ * The bean to use as data source.
+ */
+ public void setItemDataSource(T bean) {
+ setItemDataSource(new BeanItem(bean));
+ }
+
+ @Override
+ public void setItemDataSource(Item item) {
+ if (!(item instanceof BeanItem)) {
+ throw new RuntimeException(getClass().getSimpleName()
+ + " only supports BeanItems as item data source");
+ }
+ super.setItemDataSource(item);
+ }
+
+ @Override
+ public BeanItem<T> getItemDataSource() {
+ return (BeanItem<T>) super.getItemDataSource();
+ }
+
+ @Override
+ public void bind(Field field, Object propertyId) {
+ if (getItemDataSource() != null) {
+ // The data source is set so the property must be found in the item.
+ // If it is not we try to add it.
+ try {
+ getItemProperty(propertyId);
+ } catch (BindException e) {
+ // Not found, try to add a nested property;
+ // BeanItem property ids are always strings so this is safe
+ getItemDataSource().addNestedProperty((String) propertyId);
+ }
+ }
+
+ super.bind(field, propertyId);
+ }
+
+ @Override
+ protected void configureField(Field<?> field) {
+ super.configureField(field);
+ // Add Bean validators if there are annotations
+ if (isBeanValidationImplementationAvailable()) {
+ BeanValidator validator = new BeanValidator(beanType,
+ getPropertyId(field).toString());
+ field.addValidator(validator);
+ if (field.getLocale() != null) {
+ validator.setLocale(field.getLocale());
+ }
+ }
+ }
+
+ /**
+ * Checks whether a bean validation implementation (e.g. Hibernate Validator
+ * or Apache Bean Validation) is available.
+ *
+ * TODO move this method to some more generic location
+ *
+ * @return true if a JSR-303 bean validation implementation is available
+ */
+ protected static boolean isBeanValidationImplementationAvailable() {
+ if (beanValidationImplementationAvailable != null) {
+ return beanValidationImplementationAvailable;
+ }
+ try {
+ Class<?> validationClass = Class
+ .forName("javax.validation.Validation");
+ Method buildFactoryMethod = validationClass
+ .getMethod("buildDefaultValidatorFactory");
+ Object factory = buildFactoryMethod.invoke(null);
+ beanValidationImplementationAvailable = (factory != null);
+ } catch (Exception e) {
+ // no bean validation implementation available
+ beanValidationImplementationAvailable = false;
+ }
+ return beanValidationImplementationAvailable;
+ }
+} \ No newline at end of file
diff --git a/server/src/com/vaadin/data/fieldgroup/Caption.java b/server/src/com/vaadin/data/fieldgroup/Caption.java
new file mode 100644
index 0000000000..b990b720cd
--- /dev/null
+++ b/server/src/com/vaadin/data/fieldgroup/Caption.java
@@ -0,0 +1,15 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.fieldgroup;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Target({ ElementType.FIELD })
+@Retention(RetentionPolicy.RUNTIME)
+public @interface Caption {
+ String value();
+}
diff --git a/server/src/com/vaadin/data/fieldgroup/DefaultFieldGroupFieldFactory.java b/server/src/com/vaadin/data/fieldgroup/DefaultFieldGroupFieldFactory.java
new file mode 100644
index 0000000000..be0db328f2
--- /dev/null
+++ b/server/src/com/vaadin/data/fieldgroup/DefaultFieldGroupFieldFactory.java
@@ -0,0 +1,157 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.fieldgroup;
+
+import java.util.EnumSet;
+
+import com.vaadin.data.Item;
+import com.vaadin.data.fieldgroup.FieldGroup.BindException;
+import com.vaadin.ui.AbstractSelect;
+import com.vaadin.ui.AbstractTextField;
+import com.vaadin.ui.CheckBox;
+import com.vaadin.ui.ComboBox;
+import com.vaadin.ui.Field;
+import com.vaadin.ui.ListSelect;
+import com.vaadin.ui.NativeSelect;
+import com.vaadin.ui.OptionGroup;
+import com.vaadin.ui.RichTextArea;
+import com.vaadin.ui.Table;
+import com.vaadin.ui.TextField;
+
+public class DefaultFieldGroupFieldFactory implements FieldGroupFieldFactory {
+
+ public static final Object CAPTION_PROPERTY_ID = "Caption";
+
+ @Override
+ public <T extends Field> T createField(Class<?> type, Class<T> fieldType) {
+ if (Enum.class.isAssignableFrom(type)) {
+ return createEnumField(type, fieldType);
+ } else if (Boolean.class.isAssignableFrom(type)
+ || boolean.class.isAssignableFrom(type)) {
+ return createBooleanField(fieldType);
+ }
+ if (AbstractTextField.class.isAssignableFrom(fieldType)) {
+ return fieldType.cast(createAbstractTextField(fieldType
+ .asSubclass(AbstractTextField.class)));
+ } else if (fieldType == RichTextArea.class) {
+ return fieldType.cast(createRichTextArea());
+ }
+ return createDefaultField(type, fieldType);
+ }
+
+ protected RichTextArea createRichTextArea() {
+ RichTextArea rta = new RichTextArea();
+ rta.setImmediate(true);
+
+ return rta;
+ }
+
+ private <T extends Field> T createEnumField(Class<?> type,
+ Class<T> fieldType) {
+ if (AbstractSelect.class.isAssignableFrom(fieldType)) {
+ AbstractSelect s = createCompatibleSelect((Class<? extends AbstractSelect>) fieldType);
+ populateWithEnumData(s, (Class<? extends Enum>) type);
+ return (T) s;
+ }
+
+ return null;
+ }
+
+ protected AbstractSelect createCompatibleSelect(
+ Class<? extends AbstractSelect> fieldType) {
+ AbstractSelect select;
+ if (fieldType.isAssignableFrom(ListSelect.class)) {
+ select = new ListSelect();
+ select.setMultiSelect(false);
+ } else if (fieldType.isAssignableFrom(NativeSelect.class)) {
+ select = new NativeSelect();
+ } else if (fieldType.isAssignableFrom(OptionGroup.class)) {
+ select = new OptionGroup();
+ select.setMultiSelect(false);
+ } else if (fieldType.isAssignableFrom(Table.class)) {
+ Table t = new Table();
+ t.setSelectable(true);
+ select = t;
+ } else {
+ select = new ComboBox(null);
+ }
+ select.setImmediate(true);
+ select.setNullSelectionAllowed(false);
+
+ return select;
+ }
+
+ protected <T extends Field> T createBooleanField(Class<T> fieldType) {
+ if (fieldType.isAssignableFrom(CheckBox.class)) {
+ CheckBox cb = new CheckBox(null);
+ cb.setImmediate(true);
+ return (T) cb;
+ } else if (AbstractTextField.class.isAssignableFrom(fieldType)) {
+ return (T) createAbstractTextField((Class<? extends AbstractTextField>) fieldType);
+ }
+
+ return null;
+ }
+
+ protected <T extends AbstractTextField> T createAbstractTextField(
+ Class<T> fieldType) {
+ if (fieldType == AbstractTextField.class) {
+ fieldType = (Class<T>) TextField.class;
+ }
+ try {
+ T field = fieldType.newInstance();
+ field.setImmediate(true);
+ return field;
+ } catch (Exception e) {
+ throw new BindException("Could not create a field of type "
+ + fieldType, e);
+ }
+ }
+
+ /**
+ * Fallback when no specific field has been created. Typically returns a
+ * TextField.
+ *
+ * @param <T>
+ * The type of field to create
+ * @param type
+ * The type of data that should be edited
+ * @param fieldType
+ * The type of field to create
+ * @return A field capable of editing the data or null if no field could be
+ * created
+ */
+ protected <T extends Field> T createDefaultField(Class<?> type,
+ Class<T> fieldType) {
+ if (fieldType.isAssignableFrom(TextField.class)) {
+ return fieldType.cast(createAbstractTextField(TextField.class));
+ }
+ return null;
+ }
+
+ /**
+ * Populates the given select with all the enums in the given {@link Enum}
+ * class. Uses {@link Enum}.toString() for caption.
+ *
+ * @param select
+ * The select to populate
+ * @param enumClass
+ * The Enum class to use
+ */
+ protected void populateWithEnumData(AbstractSelect select,
+ Class<? extends Enum> enumClass) {
+ select.removeAllItems();
+ for (Object p : select.getContainerPropertyIds()) {
+ select.removeContainerProperty(p);
+ }
+ select.addContainerProperty(CAPTION_PROPERTY_ID, String.class, "");
+ select.setItemCaptionPropertyId(CAPTION_PROPERTY_ID);
+ @SuppressWarnings("unchecked")
+ EnumSet<?> enumSet = EnumSet.allOf(enumClass);
+ for (Object r : enumSet) {
+ Item newItem = select.addItem(r);
+ newItem.getItemProperty(CAPTION_PROPERTY_ID).setValue(r.toString());
+ }
+ }
+}
diff --git a/server/src/com/vaadin/data/fieldgroup/FieldGroup.java b/server/src/com/vaadin/data/fieldgroup/FieldGroup.java
new file mode 100644
index 0000000000..3df19f5bc9
--- /dev/null
+++ b/server/src/com/vaadin/data/fieldgroup/FieldGroup.java
@@ -0,0 +1,978 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.fieldgroup;
+
+import java.io.Serializable;
+import java.lang.reflect.InvocationTargetException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.logging.Logger;
+
+import com.vaadin.data.Item;
+import com.vaadin.data.Property;
+import com.vaadin.data.Validator.InvalidValueException;
+import com.vaadin.data.util.TransactionalPropertyWrapper;
+import com.vaadin.tools.ReflectTools;
+import com.vaadin.ui.DefaultFieldFactory;
+import com.vaadin.ui.Field;
+import com.vaadin.ui.Form;
+
+/**
+ * FieldGroup provides an easy way of binding fields to data and handling
+ * commits of these fields.
+ * <p>
+ * The functionality of FieldGroup is similar to {@link Form} but
+ * {@link FieldGroup} does not handle layouts in any way. The typical use case
+ * is to create a layout outside the FieldGroup and then use FieldGroup to bind
+ * the fields to a data source.
+ * </p>
+ * <p>
+ * {@link FieldGroup} is not a UI component so it cannot be added to a layout.
+ * Using the buildAndBind methods {@link FieldGroup} can create fields for you
+ * using a FieldGroupFieldFactory but you still have to add them to the correct
+ * position in your layout.
+ * </p>
+ *
+ * @author Vaadin Ltd
+ * @version @version@
+ * @since 7.0
+ */
+public class FieldGroup implements Serializable {
+
+ private static final Logger logger = Logger.getLogger(FieldGroup.class
+ .getName());
+
+ private Item itemDataSource;
+ private boolean buffered = true;
+
+ private boolean enabled = true;
+ private boolean readOnly = false;
+
+ private HashMap<Object, Field<?>> propertyIdToField = new HashMap<Object, Field<?>>();
+ private LinkedHashMap<Field<?>, Object> fieldToPropertyId = new LinkedHashMap<Field<?>, Object>();
+ private List<CommitHandler> commitHandlers = new ArrayList<CommitHandler>();
+
+ /**
+ * The field factory used by builder methods.
+ */
+ private FieldGroupFieldFactory fieldFactory = new DefaultFieldGroupFieldFactory();
+
+ /**
+ * Constructs a field binder. Use {@link #setItemDataSource(Item)} to set a
+ * data source for the field binder.
+ *
+ */
+ public FieldGroup() {
+
+ }
+
+ /**
+ * Constructs a field binder that uses the given data source.
+ *
+ * @param itemDataSource
+ * The data source to bind the fields to
+ */
+ public FieldGroup(Item itemDataSource) {
+ setItemDataSource(itemDataSource);
+ }
+
+ /**
+ * Updates the item that is used by this FieldBinder. Rebinds all fields to
+ * the properties in the new item.
+ *
+ * @param itemDataSource
+ * The new item to use
+ */
+ public void setItemDataSource(Item itemDataSource) {
+ this.itemDataSource = itemDataSource;
+
+ for (Field<?> f : fieldToPropertyId.keySet()) {
+ bind(f, fieldToPropertyId.get(f));
+ }
+ }
+
+ /**
+ * Gets the item used by this FieldBinder. Note that you must call
+ * {@link #commit()} for the item to be updated unless buffered mode has
+ * been switched off.
+ *
+ * @see #setBuffered(boolean)
+ * @see #commit()
+ *
+ * @return The item used by this FieldBinder
+ */
+ public Item getItemDataSource() {
+ return itemDataSource;
+ }
+
+ /**
+ * Checks the buffered mode for the bound fields.
+ * <p>
+ *
+ * @see #setBuffered(boolean) for more details on buffered mode
+ *
+ * @see Field#isBuffered()
+ * @return true if buffered mode is on, false otherwise
+ *
+ */
+ public boolean isBuffered() {
+ return buffered;
+ }
+
+ /**
+ * Sets the buffered mode for the bound fields.
+ * <p>
+ * When buffered mode is on the item will not be updated until
+ * {@link #commit()} is called. If buffered mode is off the item will be
+ * updated once the fields are updated.
+ * </p>
+ * <p>
+ * The default is to use buffered mode.
+ * </p>
+ *
+ * @see Field#setBuffered(boolean)
+ * @param buffered
+ * true to turn on buffered mode, false otherwise
+ */
+ public void setBuffered(boolean buffered) {
+ if (buffered == this.buffered) {
+ return;
+ }
+
+ this.buffered = buffered;
+ for (Field<?> field : getFields()) {
+ field.setBuffered(buffered);
+ }
+ }
+
+ /**
+ * Returns the enabled status for the fields.
+ * <p>
+ * Note that this will not accurately represent the enabled status of all
+ * fields if you change the enabled status of the fields through some other
+ * method than {@link #setEnabled(boolean)}.
+ *
+ * @return true if the fields are enabled, false otherwise
+ */
+ public boolean isEnabled() {
+ return enabled;
+ }
+
+ /**
+ * Updates the enabled state of all bound fields.
+ *
+ * @param fieldsEnabled
+ * true to enable all bound fields, false to disable them
+ */
+ public void setEnabled(boolean fieldsEnabled) {
+ enabled = fieldsEnabled;
+ for (Field<?> field : getFields()) {
+ field.setEnabled(fieldsEnabled);
+ }
+ }
+
+ /**
+ * Returns the read only status for the fields.
+ * <p>
+ * Note that this will not accurately represent the read only status of all
+ * fields if you change the read only status of the fields through some
+ * other method than {@link #setReadOnly(boolean)}.
+ *
+ * @return true if the fields are set to read only, false otherwise
+ */
+ public boolean isReadOnly() {
+ return readOnly;
+ }
+
+ /**
+ * Updates the read only state of all bound fields.
+ *
+ * @param fieldsReadOnly
+ * true to set all bound fields to read only, false to set them
+ * to read write
+ */
+ public void setReadOnly(boolean fieldsReadOnly) {
+ readOnly = fieldsReadOnly;
+ }
+
+ /**
+ * Returns a collection of all fields that have been bound.
+ * <p>
+ * The fields are not returned in any specific order.
+ * </p>
+ *
+ * @return A collection with all bound Fields
+ */
+ public Collection<Field<?>> getFields() {
+ return fieldToPropertyId.keySet();
+ }
+
+ /**
+ * Binds the field with the given propertyId from the current item. If an
+ * item has not been set then the binding is postponed until the item is set
+ * using {@link #setItemDataSource(Item)}.
+ * <p>
+ * This method also adds validators when applicable.
+ * </p>
+ *
+ * @param field
+ * The field to bind
+ * @param propertyId
+ * The propertyId to bind to the field
+ * @throws BindException
+ * If the property id is already bound to another field by this
+ * field binder
+ */
+ public void bind(Field<?> field, Object propertyId) throws BindException {
+ if (propertyIdToField.containsKey(propertyId)
+ && propertyIdToField.get(propertyId) != field) {
+ throw new BindException("Property id " + propertyId
+ + " is already bound to another field");
+ }
+ fieldToPropertyId.put(field, propertyId);
+ propertyIdToField.put(propertyId, field);
+ if (itemDataSource == null) {
+ // Will be bound when data source is set
+ return;
+ }
+
+ field.setPropertyDataSource(wrapInTransactionalProperty(getItemProperty(propertyId)));
+ configureField(field);
+ }
+
+ private <T> Property.Transactional<T> wrapInTransactionalProperty(
+ Property<T> itemProperty) {
+ return new TransactionalPropertyWrapper<T>(itemProperty);
+ }
+
+ /**
+ * Gets the property with the given property id from the item.
+ *
+ * @param propertyId
+ * The id if the property to find
+ * @return The property with the given id from the item
+ * @throws BindException
+ * If the property was not found in the item or no item has been
+ * set
+ */
+ protected Property<?> getItemProperty(Object propertyId)
+ throws BindException {
+ Item item = getItemDataSource();
+ if (item == null) {
+ throw new BindException("Could not lookup property with id "
+ + propertyId + " as no item has been set");
+ }
+ Property<?> p = item.getItemProperty(propertyId);
+ if (p == null) {
+ throw new BindException("A property with id " + propertyId
+ + " was not found in the item");
+ }
+ return p;
+ }
+
+ /**
+ * Detaches the field from its property id and removes it from this
+ * FieldBinder.
+ * <p>
+ * Note that the field is not detached from its property data source if it
+ * is no longer connected to the same property id it was bound to using this
+ * FieldBinder.
+ *
+ * @param field
+ * The field to detach
+ * @throws BindException
+ * If the field is not bound by this field binder or not bound
+ * to the correct property id
+ */
+ public void unbind(Field<?> field) throws BindException {
+ Object propertyId = fieldToPropertyId.get(field);
+ if (propertyId == null) {
+ throw new BindException(
+ "The given field is not part of this FieldBinder");
+ }
+
+ Property fieldDataSource = field.getPropertyDataSource();
+ if (fieldDataSource instanceof TransactionalPropertyWrapper) {
+ fieldDataSource = ((TransactionalPropertyWrapper) fieldDataSource)
+ .getWrappedProperty();
+ }
+ if (fieldDataSource == getItemProperty(propertyId)) {
+ field.setPropertyDataSource(null);
+ }
+ fieldToPropertyId.remove(field);
+ propertyIdToField.remove(propertyId);
+ }
+
+ /**
+ * Configures a field with the settings set for this FieldBinder.
+ * <p>
+ * By default this updates the buffered, read only and enabled state of the
+ * field. Also adds validators when applicable.
+ *
+ * @param field
+ * The field to update
+ */
+ protected void configureField(Field<?> field) {
+ field.setBuffered(isBuffered());
+
+ field.setEnabled(isEnabled());
+ field.setReadOnly(isReadOnly());
+ }
+
+ /**
+ * Gets the type of the property with the given property id.
+ *
+ * @param propertyId
+ * The propertyId. Must be find
+ * @return The type of the property
+ */
+ protected Class<?> getPropertyType(Object propertyId) throws BindException {
+ if (getItemDataSource() == null) {
+ throw new BindException(
+ "Property type for '"
+ + propertyId
+ + "' could not be determined. No item data source has been set.");
+ }
+ Property<?> p = getItemDataSource().getItemProperty(propertyId);
+ if (p == null) {
+ throw new BindException(
+ "Property type for '"
+ + propertyId
+ + "' could not be determined. No property with that id was found.");
+ }
+
+ return p.getType();
+ }
+
+ /**
+ * Returns a collection of all property ids that have been bound to fields.
+ * <p>
+ * Note that this will return property ids even before the item has been
+ * set. In that case it returns the property ids that will be bound once the
+ * item is set.
+ * </p>
+ * <p>
+ * No guarantee is given for the order of the property ids
+ * </p>
+ *
+ * @return A collection of bound property ids
+ */
+ public Collection<Object> getBoundPropertyIds() {
+ return Collections.unmodifiableCollection(propertyIdToField.keySet());
+ }
+
+ /**
+ * Returns a collection of all property ids that exist in the item set using
+ * {@link #setItemDataSource(Item)} but have not been bound to fields.
+ * <p>
+ * Will always return an empty collection before an item has been set using
+ * {@link #setItemDataSource(Item)}.
+ * </p>
+ * <p>
+ * No guarantee is given for the order of the property ids
+ * </p>
+ *
+ * @return A collection of property ids that have not been bound to fields
+ */
+ public Collection<Object> getUnboundPropertyIds() {
+ if (getItemDataSource() == null) {
+ return new ArrayList<Object>();
+ }
+ List<Object> unboundPropertyIds = new ArrayList<Object>();
+ unboundPropertyIds.addAll(getItemDataSource().getItemPropertyIds());
+ unboundPropertyIds.removeAll(propertyIdToField.keySet());
+ return unboundPropertyIds;
+ }
+
+ /**
+ * Commits all changes done to the bound fields.
+ * <p>
+ * Calls all {@link CommitHandler}s before and after committing the field
+ * changes to the item data source. The whole commit is aborted and state is
+ * restored to what it was before commit was called if any
+ * {@link CommitHandler} throws a CommitException or there is a problem
+ * committing the fields
+ *
+ * @throws CommitException
+ * If the commit was aborted
+ */
+ public void commit() throws CommitException {
+ if (!isBuffered()) {
+ // Not using buffered mode, nothing to do
+ return;
+ }
+ for (Field<?> f : fieldToPropertyId.keySet()) {
+ ((Property.Transactional<?>) f.getPropertyDataSource())
+ .startTransaction();
+ }
+ try {
+ firePreCommitEvent();
+ // Commit the field values to the properties
+ for (Field<?> f : fieldToPropertyId.keySet()) {
+ f.commit();
+ }
+ firePostCommitEvent();
+
+ // Commit the properties
+ for (Field<?> f : fieldToPropertyId.keySet()) {
+ ((Property.Transactional<?>) f.getPropertyDataSource())
+ .commit();
+ }
+
+ } catch (Exception e) {
+ for (Field<?> f : fieldToPropertyId.keySet()) {
+ try {
+ ((Property.Transactional<?>) f.getPropertyDataSource())
+ .rollback();
+ } catch (Exception rollbackException) {
+ // FIXME: What to do ?
+ }
+ }
+
+ throw new CommitException("Commit failed", e);
+ }
+
+ }
+
+ /**
+ * Sends a preCommit event to all registered commit handlers
+ *
+ * @throws CommitException
+ * If the commit should be aborted
+ */
+ private void firePreCommitEvent() throws CommitException {
+ CommitHandler[] handlers = commitHandlers
+ .toArray(new CommitHandler[commitHandlers.size()]);
+
+ for (CommitHandler handler : handlers) {
+ handler.preCommit(new CommitEvent(this));
+ }
+ }
+
+ /**
+ * Sends a postCommit event to all registered commit handlers
+ *
+ * @throws CommitException
+ * If the commit should be aborted
+ */
+ private void firePostCommitEvent() throws CommitException {
+ CommitHandler[] handlers = commitHandlers
+ .toArray(new CommitHandler[commitHandlers.size()]);
+
+ for (CommitHandler handler : handlers) {
+ handler.postCommit(new CommitEvent(this));
+ }
+ }
+
+ /**
+ * Discards all changes done to the bound fields.
+ * <p>
+ * Only has effect if buffered mode is used.
+ *
+ */
+ public void discard() {
+ for (Field<?> f : fieldToPropertyId.keySet()) {
+ try {
+ f.discard();
+ } catch (Exception e) {
+ // TODO: handle exception
+ // What can we do if discard fails other than try to discard all
+ // other fields?
+ }
+ }
+ }
+
+ /**
+ * Returns the field that is bound to the given property id
+ *
+ * @param propertyId
+ * The property id to use to lookup the field
+ * @return The field that is bound to the property id or null if no field is
+ * bound to that property id
+ */
+ public Field<?> getField(Object propertyId) {
+ return propertyIdToField.get(propertyId);
+ }
+
+ /**
+ * Returns the property id that is bound to the given field
+ *
+ * @param field
+ * The field to use to lookup the property id
+ * @return The property id that is bound to the field or null if the field
+ * is not bound to any property id by this FieldBinder
+ */
+ public Object getPropertyId(Field<?> field) {
+ return fieldToPropertyId.get(field);
+ }
+
+ /**
+ * Adds a commit handler.
+ * <p>
+ * The commit handler is called before the field values are committed to the
+ * item ( {@link CommitHandler#preCommit(CommitEvent)}) and after the item
+ * has been updated ({@link CommitHandler#postCommit(CommitEvent)}). If a
+ * {@link CommitHandler} throws a CommitException the whole commit is
+ * aborted and the fields retain their old values.
+ *
+ * @param commitHandler
+ * The commit handler to add
+ */
+ public void addCommitHandler(CommitHandler commitHandler) {
+ commitHandlers.add(commitHandler);
+ }
+
+ /**
+ * Removes the given commit handler.
+ *
+ * @see #addCommitHandler(CommitHandler)
+ *
+ * @param commitHandler
+ * The commit handler to remove
+ */
+ public void removeCommitHandler(CommitHandler commitHandler) {
+ commitHandlers.remove(commitHandler);
+ }
+
+ /**
+ * Returns a list of all commit handlers for this {@link FieldGroup}.
+ * <p>
+ * Use {@link #addCommitHandler(CommitHandler)} and
+ * {@link #removeCommitHandler(CommitHandler)} to register or unregister a
+ * commit handler.
+ *
+ * @return A collection of commit handlers
+ */
+ protected Collection<CommitHandler> getCommitHandlers() {
+ return Collections.unmodifiableCollection(commitHandlers);
+ }
+
+ /**
+ * CommitHandlers are used by {@link FieldGroup#commit()} as part of the
+ * commit transactions. CommitHandlers can perform custom operations as part
+ * of the commit and cause the commit to be aborted by throwing a
+ * {@link CommitException}.
+ */
+ public interface CommitHandler extends Serializable {
+ /**
+ * Called before changes are committed to the field and the item is
+ * updated.
+ * <p>
+ * Throw a {@link CommitException} to abort the commit.
+ *
+ * @param commitEvent
+ * An event containing information regarding the commit
+ * @throws CommitException
+ * if the commit should be aborted
+ */
+ public void preCommit(CommitEvent commitEvent) throws CommitException;
+
+ /**
+ * Called after changes are committed to the fields and the item is
+ * updated..
+ * <p>
+ * Throw a {@link CommitException} to abort the commit.
+ *
+ * @param commitEvent
+ * An event containing information regarding the commit
+ * @throws CommitException
+ * if the commit should be aborted
+ */
+ public void postCommit(CommitEvent commitEvent) throws CommitException;
+ }
+
+ /**
+ * FIXME javadoc
+ *
+ */
+ public static class CommitEvent implements Serializable {
+ private FieldGroup fieldBinder;
+
+ private CommitEvent(FieldGroup fieldBinder) {
+ this.fieldBinder = fieldBinder;
+ }
+
+ /**
+ * Returns the field binder that this commit relates to
+ *
+ * @return The FieldBinder that is being committed.
+ */
+ public FieldGroup getFieldBinder() {
+ return fieldBinder;
+ }
+
+ }
+
+ /**
+ * Checks the validity of the bound fields.
+ * <p>
+ * Call the {@link Field#validate()} for the fields to get the individual
+ * error messages.
+ *
+ * @return true if all bound fields are valid, false otherwise.
+ */
+ public boolean isValid() {
+ try {
+ for (Field<?> field : getFields()) {
+ field.validate();
+ }
+ return true;
+ } catch (InvalidValueException e) {
+ return false;
+ }
+ }
+
+ /**
+ * Checks if any bound field has been modified.
+ *
+ * @return true if at least on field has been modified, false otherwise
+ */
+ public boolean isModified() {
+ for (Field<?> field : getFields()) {
+ if (field.isModified()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Gets the field factory for the {@link FieldGroup}. The field factory is
+ * only used when {@link FieldGroup} creates a new field.
+ *
+ * @return The field factory in use
+ *
+ */
+ public FieldGroupFieldFactory getFieldFactory() {
+ return fieldFactory;
+ }
+
+ /**
+ * Sets the field factory for the {@link FieldGroup}. The field factory is
+ * only used when {@link FieldGroup} creates a new field.
+ *
+ * @param fieldFactory
+ * The field factory to use
+ */
+ public void setFieldFactory(FieldGroupFieldFactory fieldFactory) {
+ this.fieldFactory = fieldFactory;
+ }
+
+ /**
+ * Binds member fields found in the given object.
+ * <p>
+ * This method processes all (Java) member fields whose type extends
+ * {@link Field} and that can be mapped to a property id. Property id
+ * mapping is done based on the field name or on a @{@link PropertyId}
+ * annotation on the field. All non-null fields for which a property id can
+ * be determined are bound to the property id.
+ * </p>
+ * <p>
+ * For example:
+ *
+ * <pre>
+ * public class MyForm extends VerticalLayout {
+ * private TextField firstName = new TextField("First name");
+ * @PropertyId("last")
+ * private TextField lastName = new TextField("Last name");
+ * private TextField age = new TextField("Age"); ... }
+ *
+ * MyForm myForm = new MyForm();
+ * ...
+ * fieldGroup.bindMemberFields(myForm);
+ * </pre>
+ *
+ * </p>
+ * This binds the firstName TextField to a "firstName" property in the item,
+ * lastName TextField to a "last" property and the age TextField to a "age"
+ * property.
+ *
+ * @param objectWithMemberFields
+ * The object that contains (Java) member fields to bind
+ * @throws BindException
+ * If there is a problem binding a field
+ */
+ public void bindMemberFields(Object objectWithMemberFields)
+ throws BindException {
+ buildAndBindMemberFields(objectWithMemberFields, false);
+ }
+
+ /**
+ * Binds member fields found in the given object and builds member fields
+ * that have not been initialized.
+ * <p>
+ * This method processes all (Java) member fields whose type extends
+ * {@link Field} and that can be mapped to a property id. Property id
+ * mapping is done based on the field name or on a @{@link PropertyId}
+ * annotation on the field. Fields that are not initialized (null) are built
+ * using the field factory. All non-null fields for which a property id can
+ * be determined are bound to the property id.
+ * </p>
+ * <p>
+ * For example:
+ *
+ * <pre>
+ * public class MyForm extends VerticalLayout {
+ * private TextField firstName = new TextField("First name");
+ * @PropertyId("last")
+ * private TextField lastName = new TextField("Last name");
+ * private TextField age;
+ *
+ * MyForm myForm = new MyForm();
+ * ...
+ * fieldGroup.buildAndBindMemberFields(myForm);
+ * </pre>
+ *
+ * </p>
+ * <p>
+ * This binds the firstName TextField to a "firstName" property in the item,
+ * lastName TextField to a "last" property and builds an age TextField using
+ * the field factory and then binds it to the "age" property.
+ * </p>
+ *
+ * @param objectWithMemberFields
+ * The object that contains (Java) member fields to build and
+ * bind
+ * @throws BindException
+ * If there is a problem binding or building a field
+ */
+ public void buildAndBindMemberFields(Object objectWithMemberFields)
+ throws BindException {
+ buildAndBindMemberFields(objectWithMemberFields, true);
+ }
+
+ /**
+ * Binds member fields found in the given object and optionally builds
+ * member fields that have not been initialized.
+ * <p>
+ * This method processes all (Java) member fields whose type extends
+ * {@link Field} and that can be mapped to a property id. Property id
+ * mapping is done based on the field name or on a @{@link PropertyId}
+ * annotation on the field. Fields that are not initialized (null) are built
+ * using the field factory is buildFields is true. All non-null fields for
+ * which a property id can be determined are bound to the property id.
+ * </p>
+ *
+ * @param objectWithMemberFields
+ * The object that contains (Java) member fields to build and
+ * bind
+ * @throws BindException
+ * If there is a problem binding or building a field
+ */
+ protected void buildAndBindMemberFields(Object objectWithMemberFields,
+ boolean buildFields) throws BindException {
+ Class<?> objectClass = objectWithMemberFields.getClass();
+
+ for (java.lang.reflect.Field memberField : objectClass
+ .getDeclaredFields()) {
+
+ if (!Field.class.isAssignableFrom(memberField.getType())) {
+ // Process next field
+ continue;
+ }
+
+ PropertyId propertyIdAnnotation = memberField
+ .getAnnotation(PropertyId.class);
+
+ Class<? extends Field> fieldType = (Class<? extends Field>) memberField
+ .getType();
+
+ Object propertyId = null;
+ if (propertyIdAnnotation != null) {
+ // @PropertyId(propertyId) always overrides property id
+ propertyId = propertyIdAnnotation.value();
+ } else {
+ propertyId = memberField.getName();
+ }
+
+ // Ensure that the property id exists
+ Class<?> propertyType;
+
+ try {
+ propertyType = getPropertyType(propertyId);
+ } catch (BindException e) {
+ // Property id was not found, skip this field
+ continue;
+ }
+
+ Field<?> field;
+ try {
+ // Get the field from the object
+ field = (Field<?>) ReflectTools.getJavaFieldValue(
+ objectWithMemberFields, memberField);
+ } catch (Exception e) {
+ // If we cannot determine the value, just skip the field and try
+ // the next one
+ continue;
+ }
+
+ if (field == null && buildFields) {
+ Caption captionAnnotation = memberField
+ .getAnnotation(Caption.class);
+ String caption;
+ if (captionAnnotation != null) {
+ caption = captionAnnotation.value();
+ } else {
+ caption = DefaultFieldFactory
+ .createCaptionByPropertyId(propertyId);
+ }
+
+ // Create the component (Field)
+ field = build(caption, propertyType, fieldType);
+
+ // Store it in the field
+ try {
+ ReflectTools.setJavaFieldValue(objectWithMemberFields,
+ memberField, field);
+ } catch (IllegalArgumentException e) {
+ throw new BindException("Could not assign value to field '"
+ + memberField.getName() + "'", e);
+ } catch (IllegalAccessException e) {
+ throw new BindException("Could not assign value to field '"
+ + memberField.getName() + "'", e);
+ } catch (InvocationTargetException e) {
+ throw new BindException("Could not assign value to field '"
+ + memberField.getName() + "'", e);
+ }
+ }
+
+ if (field != null) {
+ // Bind it to the property id
+ bind(field, propertyId);
+ }
+ }
+ }
+
+ public static class CommitException extends Exception {
+
+ public CommitException() {
+ super();
+ // TODO Auto-generated constructor stub
+ }
+
+ public CommitException(String message, Throwable cause) {
+ super(message, cause);
+ // TODO Auto-generated constructor stub
+ }
+
+ public CommitException(String message) {
+ super(message);
+ // TODO Auto-generated constructor stub
+ }
+
+ public CommitException(Throwable cause) {
+ super(cause);
+ // TODO Auto-generated constructor stub
+ }
+
+ }
+
+ public static class BindException extends RuntimeException {
+
+ public BindException(String message) {
+ super(message);
+ }
+
+ public BindException(String message, Throwable t) {
+ super(message, t);
+ }
+
+ }
+
+ /**
+ * Builds a field and binds it to the given property id using the field
+ * binder.
+ *
+ * @param propertyId
+ * The property id to bind to. Must be present in the field
+ * finder.
+ * @throws BindException
+ * If there is a problem while building or binding
+ * @return The created and bound field
+ */
+ public Field<?> buildAndBind(Object propertyId) throws BindException {
+ String caption = DefaultFieldFactory
+ .createCaptionByPropertyId(propertyId);
+ return buildAndBind(caption, propertyId);
+ }
+
+ /**
+ * Builds a field using the given caption and binds it to the given property
+ * id using the field binder.
+ *
+ * @param caption
+ * The caption for the field
+ * @param propertyId
+ * The property id to bind to. Must be present in the field
+ * finder.
+ * @throws BindException
+ * If there is a problem while building or binding
+ * @return The created and bound field. Can be any type of {@link Field}.
+ */
+ public Field<?> buildAndBind(String caption, Object propertyId)
+ throws BindException {
+ Class<?> type = getPropertyType(propertyId);
+ return buildAndBind(caption, propertyId, Field.class);
+
+ }
+
+ /**
+ * Builds a field using the given caption and binds it to the given property
+ * id using the field binder. Ensures the new field is of the given type.
+ *
+ * @param caption
+ * The caption for the field
+ * @param propertyId
+ * The property id to bind to. Must be present in the field
+ * finder.
+ * @throws BindException
+ * If the field could not be created
+ * @return The created and bound field. Can be any type of {@link Field}.
+ */
+
+ public <T extends Field> T buildAndBind(String caption, Object propertyId,
+ Class<T> fieldType) throws BindException {
+ Class<?> type = getPropertyType(propertyId);
+
+ T field = build(caption, type, fieldType);
+ bind(field, propertyId);
+
+ return field;
+ }
+
+ /**
+ * Creates a field based on the given data type.
+ * <p>
+ * The data type is the type that we want to edit using the field. The field
+ * type is the type of field we want to create, can be {@link Field} if any
+ * Field is good.
+ * </p>
+ *
+ * @param caption
+ * The caption for the new field
+ * @param dataType
+ * The data model type that we want to edit using the field
+ * @param fieldType
+ * The type of field that we want to create
+ * @return A Field capable of editing the given type
+ * @throws BindException
+ * If the field could not be created
+ */
+ protected <T extends Field> T build(String caption, Class<?> dataType,
+ Class<T> fieldType) throws BindException {
+ T field = getFieldFactory().createField(dataType, fieldType);
+ if (field == null) {
+ throw new BindException("Unable to build a field of type "
+ + fieldType.getName() + " for editing "
+ + dataType.getName());
+ }
+
+ field.setCaption(caption);
+ return field;
+ }
+} \ No newline at end of file
diff --git a/server/src/com/vaadin/data/fieldgroup/FieldGroupFieldFactory.java b/server/src/com/vaadin/data/fieldgroup/FieldGroupFieldFactory.java
new file mode 100644
index 0000000000..80c012cbdc
--- /dev/null
+++ b/server/src/com/vaadin/data/fieldgroup/FieldGroupFieldFactory.java
@@ -0,0 +1,31 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.fieldgroup;
+
+import java.io.Serializable;
+
+import com.vaadin.ui.Field;
+
+/**
+ * Factory interface for creating new Field-instances based on the data type
+ * that should be edited.
+ *
+ * @author Vaadin Ltd.
+ * @version @version@
+ * @since 7.0
+ */
+public interface FieldGroupFieldFactory extends Serializable {
+ /**
+ * Creates a field based on the data type that we want to edit
+ *
+ * @param dataType
+ * The type that we want to edit using the field
+ * @param fieldType
+ * The type of field we want to create. If set to {@link Field}
+ * then any type of field is accepted
+ * @return A field that can be assigned to the given fieldType and that is
+ * capable of editing the given type of data
+ */
+ <T extends Field> T createField(Class<?> dataType, Class<T> fieldType);
+}
diff --git a/server/src/com/vaadin/data/fieldgroup/PropertyId.java b/server/src/com/vaadin/data/fieldgroup/PropertyId.java
new file mode 100644
index 0000000000..268047401d
--- /dev/null
+++ b/server/src/com/vaadin/data/fieldgroup/PropertyId.java
@@ -0,0 +1,15 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.fieldgroup;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Target({ ElementType.FIELD })
+@Retention(RetentionPolicy.RUNTIME)
+public @interface PropertyId {
+ String value();
+}
diff --git a/server/src/com/vaadin/data/package.html b/server/src/com/vaadin/data/package.html
new file mode 100644
index 0000000000..a14ea1ac88
--- /dev/null
+++ b/server/src/com/vaadin/data/package.html
@@ -0,0 +1,49 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
+<html>
+<head>
+</head>
+
+<body bgcolor="white">
+
+<p>Contains interfaces for the data layer, mainly for binding typed
+data and data collections to components, and for validating data.</p>
+
+<h2>Data binding</h2>
+
+<p>The package contains a three-tiered structure for typed data
+objects and collections of them:</p>
+
+<ul>
+ <li>A {@link com.vaadin.data.Property Property} represents a
+ single, typed data value.
+
+ <li>An {@link com.vaadin.data.Item Item} embodies a set of <i>Properties</i>.
+ A locally unique (inside the {@link com.vaadin.data.Item Item})
+ Property identifier corresponds to each Property inside the Item.</li>
+ <li>A {@link com.vaadin.data.Container Container} contains a set
+ of Items, each corresponding to a locally unique Item identifier. Note
+ that Container imposes a few restrictions on the data stored in it, see
+ {@link com.vaadin.data.Container Container} for further information.</li>
+</ul>
+
+<p>For more information on the data model, see the <a
+ href="http://vaadin.com/book/-/page/datamodel.html">Data model
+chapter</a> in Book of Vaadin.</p>
+
+<h2>Buffering</h2>
+
+<p>A {@link com.vaadin.data.Buffered Buffered} implementor is able
+to track and buffer changes and commit or discard them later.</p>
+
+<h2>Validation</h2>
+
+<p>{@link com.vaadin.data.Validator Validator} implementations are
+used to validate data, typically the value of a {@link
+com.vaadin.ui.Field Field}. One or more {@link com.vaadin.data.Validator
+Validators} can be added to a {@link com.vaadin.data.Validatable
+Validatable} implementor and then used to validate the value of the
+Validatable. </p>
+
+<!-- Put @see and @since tags down here. -->
+</body>
+</html>
diff --git a/server/src/com/vaadin/data/util/AbstractBeanContainer.java b/server/src/com/vaadin/data/util/AbstractBeanContainer.java
new file mode 100644
index 0000000000..2f428d2cb6
--- /dev/null
+++ b/server/src/com/vaadin/data/util/AbstractBeanContainer.java
@@ -0,0 +1,856 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util;
+
+import java.io.Serializable;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import com.vaadin.data.Container;
+import com.vaadin.data.Container.Filterable;
+import com.vaadin.data.Container.PropertySetChangeNotifier;
+import com.vaadin.data.Container.SimpleFilterable;
+import com.vaadin.data.Container.Sortable;
+import com.vaadin.data.Item;
+import com.vaadin.data.Property;
+import com.vaadin.data.Property.ValueChangeEvent;
+import com.vaadin.data.Property.ValueChangeListener;
+import com.vaadin.data.Property.ValueChangeNotifier;
+import com.vaadin.data.util.MethodProperty.MethodException;
+import com.vaadin.data.util.filter.SimpleStringFilter;
+import com.vaadin.data.util.filter.UnsupportedFilterException;
+
+/**
+ * An abstract base class for in-memory containers for JavaBeans.
+ *
+ * <p>
+ * The properties of the container are determined automatically by introspecting
+ * the used JavaBean class and explicitly adding or removing properties is not
+ * supported. Only beans of the same type can be added to the container.
+ * </p>
+ *
+ * <p>
+ * Subclasses should implement any public methods adding items to the container,
+ * typically calling the protected methods {@link #addItem(Object, Object)},
+ * {@link #addItemAfter(Object, Object, Object)} and
+ * {@link #addItemAt(int, Object, Object)}.
+ * </p>
+ *
+ * @param <IDTYPE>
+ * The type of the item identifier
+ * @param <BEANTYPE>
+ * The type of the Bean
+ *
+ * @since 6.5
+ */
+public abstract class AbstractBeanContainer<IDTYPE, BEANTYPE> extends
+ AbstractInMemoryContainer<IDTYPE, String, BeanItem<BEANTYPE>> implements
+ Filterable, SimpleFilterable, Sortable, ValueChangeListener,
+ PropertySetChangeNotifier {
+
+ /**
+ * Resolver that maps beans to their (item) identifiers, removing the need
+ * to explicitly specify item identifiers when there is no need to customize
+ * this.
+ *
+ * Note that beans can also be added with an explicit id even if a resolver
+ * has been set.
+ *
+ * @param <IDTYPE>
+ * @param <BEANTYPE>
+ *
+ * @since 6.5
+ */
+ public static interface BeanIdResolver<IDTYPE, BEANTYPE> extends
+ Serializable {
+ /**
+ * Return the item identifier for a bean.
+ *
+ * @param bean
+ * @return
+ */
+ public IDTYPE getIdForBean(BEANTYPE bean);
+ }
+
+ /**
+ * A item identifier resolver that returns the value of a bean property.
+ *
+ * The bean must have a getter for the property, and the getter must return
+ * an object of type IDTYPE.
+ */
+ protected class PropertyBasedBeanIdResolver implements
+ BeanIdResolver<IDTYPE, BEANTYPE> {
+
+ private final Object propertyId;
+
+ public PropertyBasedBeanIdResolver(Object propertyId) {
+ if (propertyId == null) {
+ throw new IllegalArgumentException(
+ "Property identifier must not be null");
+ }
+ this.propertyId = propertyId;
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public IDTYPE getIdForBean(BEANTYPE bean)
+ throws IllegalArgumentException {
+ VaadinPropertyDescriptor<BEANTYPE> pd = model.get(propertyId);
+ if (null == pd) {
+ throw new IllegalStateException("Property " + propertyId
+ + " not found");
+ }
+ try {
+ Property<IDTYPE> property = (Property<IDTYPE>) pd
+ .createProperty(bean);
+ return property.getValue();
+ } catch (MethodException e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+ }
+
+ /**
+ * The resolver that finds the item ID for a bean, or null not to use
+ * automatic resolving.
+ *
+ * Methods that add a bean without specifying an ID must not be called if no
+ * resolver has been set.
+ */
+ private BeanIdResolver<IDTYPE, BEANTYPE> beanIdResolver = null;
+
+ /**
+ * Maps all item ids in the container (including filtered) to their
+ * corresponding BeanItem.
+ */
+ private final Map<IDTYPE, BeanItem<BEANTYPE>> itemIdToItem = new HashMap<IDTYPE, BeanItem<BEANTYPE>>();
+
+ /**
+ * The type of the beans in the container.
+ */
+ private final Class<? super BEANTYPE> type;
+
+ /**
+ * A description of the properties found in beans of type {@link #type}.
+ * Determines the property ids that are present in the container.
+ */
+ private LinkedHashMap<String, VaadinPropertyDescriptor<BEANTYPE>> model;
+
+ /**
+ * Constructs a {@code AbstractBeanContainer} for beans of the given type.
+ *
+ * @param type
+ * the type of the beans that will be added to the container.
+ * @throws IllegalArgumentException
+ * If {@code type} is null
+ */
+ protected AbstractBeanContainer(Class<? super BEANTYPE> type) {
+ if (type == null) {
+ throw new IllegalArgumentException(
+ "The bean type passed to AbstractBeanContainer must not be null");
+ }
+ this.type = type;
+ model = BeanItem.getPropertyDescriptors((Class<BEANTYPE>) type);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container#getType(java.lang.Object)
+ */
+ @Override
+ public Class<?> getType(Object propertyId) {
+ return model.get(propertyId).getPropertyType();
+ }
+
+ /**
+ * Create a BeanItem for a bean using pre-parsed bean metadata (based on
+ * {@link #getBeanType()}).
+ *
+ * @param bean
+ * @return created {@link BeanItem} or null if bean is null
+ */
+ protected BeanItem<BEANTYPE> createBeanItem(BEANTYPE bean) {
+ return bean == null ? null : new BeanItem<BEANTYPE>(bean, model);
+ }
+
+ /**
+ * Returns the type of beans this Container can contain.
+ *
+ * This comes from the bean type constructor parameter, and bean metadata
+ * (including container properties) is based on this.
+ *
+ * @return
+ */
+ public Class<? super BEANTYPE> getBeanType() {
+ return type;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container#getContainerPropertyIds()
+ */
+ @Override
+ public Collection<String> getContainerPropertyIds() {
+ return model.keySet();
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container#removeAllItems()
+ */
+ @Override
+ public boolean removeAllItems() {
+ int origSize = size();
+
+ internalRemoveAllItems();
+
+ // detach listeners from all Items
+ for (Item item : itemIdToItem.values()) {
+ removeAllValueChangeListeners(item);
+ }
+ itemIdToItem.clear();
+
+ // fire event only if the visible view changed, regardless of whether
+ // filtered out items were removed or not
+ if (origSize != 0) {
+ fireItemSetChange();
+ }
+
+ return true;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container#getItem(java.lang.Object)
+ */
+ @Override
+ public BeanItem<BEANTYPE> getItem(Object itemId) {
+ // TODO return only if visible?
+ return getUnfilteredItem(itemId);
+ }
+
+ @Override
+ protected BeanItem<BEANTYPE> getUnfilteredItem(Object itemId) {
+ return itemIdToItem.get(itemId);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container#getItemIds()
+ */
+ @Override
+ @SuppressWarnings("unchecked")
+ public List<IDTYPE> getItemIds() {
+ return (List<IDTYPE>) super.getItemIds();
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container#getContainerProperty(java.lang.Object,
+ * java.lang.Object)
+ */
+ @Override
+ public Property<?> getContainerProperty(Object itemId, Object propertyId) {
+ Item item = getItem(itemId);
+ if (item == null) {
+ return null;
+ }
+ return item.getItemProperty(propertyId);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container#removeItem(java.lang.Object)
+ */
+ @Override
+ public boolean removeItem(Object itemId) {
+ // TODO should also remove items that are filtered out
+ int origSize = size();
+ Item item = getItem(itemId);
+ int position = indexOfId(itemId);
+
+ if (internalRemoveItem(itemId)) {
+ // detach listeners from Item
+ removeAllValueChangeListeners(item);
+
+ // remove item
+ itemIdToItem.remove(itemId);
+
+ // fire event only if the visible view changed, regardless of
+ // whether filtered out items were removed or not
+ if (size() != origSize) {
+ fireItemRemoved(position, itemId);
+ }
+
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Re-filter the container when one of the monitored properties changes.
+ */
+ @Override
+ public void valueChange(ValueChangeEvent event) {
+ // if a property that is used in a filter is changed, refresh filtering
+ filterAll();
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.data.Container.Filterable#addContainerFilter(java.lang.Object,
+ * java.lang.String, boolean, boolean)
+ */
+ @Override
+ public void addContainerFilter(Object propertyId, String filterString,
+ boolean ignoreCase, boolean onlyMatchPrefix) {
+ try {
+ addFilter(new SimpleStringFilter(propertyId, filterString,
+ ignoreCase, onlyMatchPrefix));
+ } catch (UnsupportedFilterException e) {
+ // the filter instance created here is always valid for in-memory
+ // containers
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container.Filterable#removeAllContainerFilters()
+ */
+ @Override
+ public void removeAllContainerFilters() {
+ if (!getFilters().isEmpty()) {
+ for (Item item : itemIdToItem.values()) {
+ removeAllValueChangeListeners(item);
+ }
+ removeAllFilters();
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.data.Container.Filterable#removeContainerFilters(java.lang
+ * .Object)
+ */
+ @Override
+ public void removeContainerFilters(Object propertyId) {
+ Collection<Filter> removedFilters = super.removeFilters(propertyId);
+ if (!removedFilters.isEmpty()) {
+ // stop listening to change events for the property
+ for (Item item : itemIdToItem.values()) {
+ removeValueChangeListener(item, propertyId);
+ }
+ }
+ }
+
+ @Override
+ public void addContainerFilter(Filter filter)
+ throws UnsupportedFilterException {
+ addFilter(filter);
+ }
+
+ @Override
+ public void removeContainerFilter(Filter filter) {
+ removeFilter(filter);
+ }
+
+ /**
+ * Make this container listen to the given property provided it notifies
+ * when its value changes.
+ *
+ * @param item
+ * The {@link Item} that contains the property
+ * @param propertyId
+ * The id of the property
+ */
+ private void addValueChangeListener(Item item, Object propertyId) {
+ Property<?> property = item.getItemProperty(propertyId);
+ if (property instanceof ValueChangeNotifier) {
+ // avoid multiple notifications for the same property if
+ // multiple filters are in use
+ ValueChangeNotifier notifier = (ValueChangeNotifier) property;
+ notifier.removeListener(this);
+ notifier.addListener(this);
+ }
+ }
+
+ /**
+ * Remove this container as a listener for the given property.
+ *
+ * @param item
+ * The {@link Item} that contains the property
+ * @param propertyId
+ * The id of the property
+ */
+ private void removeValueChangeListener(Item item, Object propertyId) {
+ Property<?> property = item.getItemProperty(propertyId);
+ if (property instanceof ValueChangeNotifier) {
+ ((ValueChangeNotifier) property).removeListener(this);
+ }
+ }
+
+ /**
+ * Remove this contains as a listener for all the properties in the given
+ * {@link Item}.
+ *
+ * @param item
+ * The {@link Item} that contains the properties
+ */
+ private void removeAllValueChangeListeners(Item item) {
+ for (Object propertyId : item.getItemPropertyIds()) {
+ removeValueChangeListener(item, propertyId);
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container.Sortable#getSortableContainerPropertyIds()
+ */
+ @Override
+ public Collection<?> getSortableContainerPropertyIds() {
+ return getSortablePropertyIds();
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container.Sortable#sort(java.lang.Object[],
+ * boolean[])
+ */
+ @Override
+ public void sort(Object[] propertyId, boolean[] ascending) {
+ sortContainer(propertyId, ascending);
+ }
+
+ @Override
+ public ItemSorter getItemSorter() {
+ return super.getItemSorter();
+ }
+
+ @Override
+ public void setItemSorter(ItemSorter itemSorter) {
+ super.setItemSorter(itemSorter);
+ }
+
+ @Override
+ protected void registerNewItem(int position, IDTYPE itemId,
+ BeanItem<BEANTYPE> item) {
+ itemIdToItem.put(itemId, item);
+
+ // add listeners to be able to update filtering on property
+ // changes
+ for (Filter filter : getFilters()) {
+ for (String propertyId : getContainerPropertyIds()) {
+ if (filter.appliesToProperty(propertyId)) {
+ // addValueChangeListener avoids adding duplicates
+ addValueChangeListener(item, propertyId);
+ }
+ }
+ }
+ }
+
+ /**
+ * Check that a bean can be added to the container (is of the correct type
+ * for the container).
+ *
+ * @param bean
+ * @return
+ */
+ private boolean validateBean(BEANTYPE bean) {
+ return bean != null && getBeanType().isAssignableFrom(bean.getClass());
+ }
+
+ /**
+ * Adds the bean to the Container.
+ *
+ * Note: the behavior of this method changed in Vaadin 6.6 - now items are
+ * added at the very end of the unfiltered container and not after the last
+ * visible item if filtering is used.
+ *
+ * @see com.vaadin.data.Container#addItem(Object)
+ */
+ protected BeanItem<BEANTYPE> addItem(IDTYPE itemId, BEANTYPE bean) {
+ if (!validateBean(bean)) {
+ return null;
+ }
+ return internalAddItemAtEnd(itemId, createBeanItem(bean), true);
+ }
+
+ /**
+ * Adds the bean after the given bean.
+ *
+ * @see com.vaadin.data.Container.Ordered#addItemAfter(Object, Object)
+ */
+ protected BeanItem<BEANTYPE> addItemAfter(IDTYPE previousItemId,
+ IDTYPE newItemId, BEANTYPE bean) {
+ if (!validateBean(bean)) {
+ return null;
+ }
+ return internalAddItemAfter(previousItemId, newItemId,
+ createBeanItem(bean), true);
+ }
+
+ /**
+ * Adds a new bean at the given index.
+ *
+ * The bean is used both as the item contents and as the item identifier.
+ *
+ * @param index
+ * Index at which the bean should be added.
+ * @param newItemId
+ * The item id for the bean to add to the container.
+ * @param bean
+ * The bean to add to the container.
+ *
+ * @return Returns the new BeanItem or null if the operation fails.
+ */
+ protected BeanItem<BEANTYPE> addItemAt(int index, IDTYPE newItemId,
+ BEANTYPE bean) {
+ if (!validateBean(bean)) {
+ return null;
+ }
+ return internalAddItemAt(index, newItemId, createBeanItem(bean), true);
+ }
+
+ /**
+ * Adds a bean to the container using the bean item id resolver to find its
+ * identifier.
+ *
+ * A bean id resolver must be set before calling this method.
+ *
+ * @see #addItem(Object, Object)
+ *
+ * @param bean
+ * the bean to add
+ * @return BeanItem<BEANTYPE> item added or null
+ * @throws IllegalStateException
+ * if no bean identifier resolver has been set
+ * @throws IllegalArgumentException
+ * if an identifier cannot be resolved for the bean
+ */
+ protected BeanItem<BEANTYPE> addBean(BEANTYPE bean)
+ throws IllegalStateException, IllegalArgumentException {
+ if (bean == null) {
+ return null;
+ }
+ IDTYPE itemId = resolveBeanId(bean);
+ if (itemId == null) {
+ throw new IllegalArgumentException(
+ "Resolved identifier for a bean must not be null");
+ }
+ return addItem(itemId, bean);
+ }
+
+ /**
+ * Adds a bean to the container after a specified item identifier, using the
+ * bean item id resolver to find its identifier.
+ *
+ * A bean id resolver must be set before calling this method.
+ *
+ * @see #addItemAfter(Object, Object, Object)
+ *
+ * @param previousItemId
+ * the identifier of the bean after which this bean should be
+ * added, null to add to the beginning
+ * @param bean
+ * the bean to add
+ * @return BeanItem<BEANTYPE> item added or null
+ * @throws IllegalStateException
+ * if no bean identifier resolver has been set
+ * @throws IllegalArgumentException
+ * if an identifier cannot be resolved for the bean
+ */
+ protected BeanItem<BEANTYPE> addBeanAfter(IDTYPE previousItemId,
+ BEANTYPE bean) throws IllegalStateException,
+ IllegalArgumentException {
+ if (bean == null) {
+ return null;
+ }
+ IDTYPE itemId = resolveBeanId(bean);
+ if (itemId == null) {
+ throw new IllegalArgumentException(
+ "Resolved identifier for a bean must not be null");
+ }
+ return addItemAfter(previousItemId, itemId, bean);
+ }
+
+ /**
+ * Adds a bean at a specified (filtered view) position in the container
+ * using the bean item id resolver to find its identifier.
+ *
+ * A bean id resolver must be set before calling this method.
+ *
+ * @see #addItemAfter(Object, Object, Object)
+ *
+ * @param index
+ * the index (in the filtered view) at which to add the item
+ * @param bean
+ * the bean to add
+ * @return BeanItem<BEANTYPE> item added or null
+ * @throws IllegalStateException
+ * if no bean identifier resolver has been set
+ * @throws IllegalArgumentException
+ * if an identifier cannot be resolved for the bean
+ */
+ protected BeanItem<BEANTYPE> addBeanAt(int index, BEANTYPE bean)
+ throws IllegalStateException, IllegalArgumentException {
+ if (bean == null) {
+ return null;
+ }
+ IDTYPE itemId = resolveBeanId(bean);
+ if (itemId == null) {
+ throw new IllegalArgumentException(
+ "Resolved identifier for a bean must not be null");
+ }
+ return addItemAt(index, itemId, bean);
+ }
+
+ /**
+ * Adds all the beans from a {@link Collection} in one operation using the
+ * bean item identifier resolver. More efficient than adding them one by
+ * one.
+ *
+ * A bean id resolver must be set before calling this method.
+ *
+ * Note: the behavior of this method changed in Vaadin 6.6 - now items are
+ * added at the very end of the unfiltered container and not after the last
+ * visible item if filtering is used.
+ *
+ * @param collection
+ * The collection of beans to add. Must not be null.
+ * @throws IllegalStateException
+ * if no bean identifier resolver has been set
+ * @throws IllegalArgumentException
+ * if the resolver returns a null itemId for one of the beans in
+ * the collection
+ */
+ protected void addAll(Collection<? extends BEANTYPE> collection)
+ throws IllegalStateException, IllegalArgumentException {
+ boolean modified = false;
+ for (BEANTYPE bean : collection) {
+ // TODO skipping invalid beans - should not allow them in javadoc?
+ if (bean == null
+ || !getBeanType().isAssignableFrom(bean.getClass())) {
+ continue;
+ }
+ IDTYPE itemId = resolveBeanId(bean);
+ if (itemId == null) {
+ throw new IllegalArgumentException(
+ "Resolved identifier for a bean must not be null");
+ }
+
+ if (internalAddItemAtEnd(itemId, createBeanItem(bean), false) != null) {
+ modified = true;
+ }
+ }
+
+ if (modified) {
+ // Filter the contents when all items have been added
+ if (isFiltered()) {
+ filterAll();
+ } else {
+ fireItemSetChange();
+ }
+ }
+ }
+
+ /**
+ * Use the bean resolver to get the identifier for a bean.
+ *
+ * @param bean
+ * @return resolved bean identifier, null if could not be resolved
+ * @throws IllegalStateException
+ * if no bean resolver is set
+ */
+ protected IDTYPE resolveBeanId(BEANTYPE bean) {
+ if (beanIdResolver == null) {
+ throw new IllegalStateException(
+ "Bean item identifier resolver is required.");
+ }
+ return beanIdResolver.getIdForBean(bean);
+ }
+
+ /**
+ * Sets the resolver that finds the item id for a bean, or null not to use
+ * automatic resolving.
+ *
+ * Methods that add a bean without specifying an id must not be called if no
+ * resolver has been set.
+ *
+ * Note that methods taking an explicit id can be used whether a resolver
+ * has been defined or not.
+ *
+ * @param beanIdResolver
+ * to use or null to disable automatic id resolution
+ */
+ protected void setBeanIdResolver(
+ BeanIdResolver<IDTYPE, BEANTYPE> beanIdResolver) {
+ this.beanIdResolver = beanIdResolver;
+ }
+
+ /**
+ * Returns the resolver that finds the item ID for a bean.
+ *
+ * @return resolver used or null if automatic item id resolving is disabled
+ */
+ public BeanIdResolver<IDTYPE, BEANTYPE> getBeanIdResolver() {
+ return beanIdResolver;
+ }
+
+ /**
+ * Create an item identifier resolver using a named bean property.
+ *
+ * @param propertyId
+ * property identifier, which must map to a getter in BEANTYPE
+ * @return created resolver
+ */
+ protected BeanIdResolver<IDTYPE, BEANTYPE> createBeanPropertyResolver(
+ Object propertyId) {
+ return new PropertyBasedBeanIdResolver(propertyId);
+ }
+
+ @Override
+ public void addListener(Container.PropertySetChangeListener listener) {
+ super.addListener(listener);
+ }
+
+ @Override
+ public void removeListener(Container.PropertySetChangeListener listener) {
+ super.removeListener(listener);
+ }
+
+ @Override
+ public boolean addContainerProperty(Object propertyId, Class<?> type,
+ Object defaultValue) throws UnsupportedOperationException {
+ throw new UnsupportedOperationException(
+ "Use addNestedContainerProperty(String) to add container properties to a "
+ + getClass().getSimpleName());
+ }
+
+ /**
+ * Adds a property for the container and all its items.
+ *
+ * Primarily for internal use, may change in future versions.
+ *
+ * @param propertyId
+ * @param propertyDescriptor
+ * @return true if the property was added
+ */
+ protected final boolean addContainerProperty(String propertyId,
+ VaadinPropertyDescriptor<BEANTYPE> propertyDescriptor) {
+ if (null == propertyId || null == propertyDescriptor) {
+ return false;
+ }
+
+ // Fails if the Property is already present
+ if (model.containsKey(propertyId)) {
+ return false;
+ }
+
+ model.put(propertyId, propertyDescriptor);
+ for (BeanItem<BEANTYPE> item : itemIdToItem.values()) {
+ item.addItemProperty(propertyId,
+ propertyDescriptor.createProperty(item.getBean()));
+ }
+
+ // Sends a change event
+ fireContainerPropertySetChange();
+
+ return true;
+ }
+
+ /**
+ * Adds a nested container property for the container, e.g.
+ * "manager.address.street".
+ *
+ * All intermediate getters must exist and must return non-null values when
+ * the property value is accessed.
+ *
+ * @see NestedMethodProperty
+ *
+ * @param propertyId
+ * @return true if the property was added
+ */
+ public boolean addNestedContainerProperty(String propertyId) {
+ return addContainerProperty(propertyId, new NestedPropertyDescriptor(
+ propertyId, type));
+ }
+
+ /**
+ * Adds a nested container properties for all sub-properties of a named
+ * property to the container. The named property itself is removed from the
+ * model as its subproperties are added.
+ *
+ * All intermediate getters must exist and must return non-null values when
+ * the property value is accessed.
+ *
+ * @see NestedMethodProperty
+ * @see #addNestedContainerProperty(String)
+ *
+ * @param propertyId
+ */
+ @SuppressWarnings("unchecked")
+ public void addNestedContainerBean(String propertyId) {
+ Class<?> propertyType = getType(propertyId);
+ LinkedHashMap<String, VaadinPropertyDescriptor<Object>> pds = BeanItem
+ .getPropertyDescriptors((Class<Object>) propertyType);
+ for (String subPropertyId : pds.keySet()) {
+ String qualifiedPropertyId = propertyId + "." + subPropertyId;
+ NestedPropertyDescriptor<BEANTYPE> pd = new NestedPropertyDescriptor<BEANTYPE>(
+ qualifiedPropertyId, (Class<BEANTYPE>) type);
+ model.put(qualifiedPropertyId, pd);
+ model.remove(propertyId);
+ for (BeanItem<BEANTYPE> item : itemIdToItem.values()) {
+ item.addItemProperty(propertyId,
+ pd.createProperty(item.getBean()));
+ item.removeItemProperty(propertyId);
+ }
+ }
+
+ // Sends a change event
+ fireContainerPropertySetChange();
+ }
+
+ @Override
+ public boolean removeContainerProperty(Object propertyId)
+ throws UnsupportedOperationException {
+ // Fails if the Property is not present
+ if (!model.containsKey(propertyId)) {
+ return false;
+ }
+
+ // Removes the Property to Property list and types
+ model.remove(propertyId);
+
+ // If remove the Property from all Items
+ for (final Iterator<IDTYPE> i = getAllItemIds().iterator(); i.hasNext();) {
+ getUnfilteredItem(i.next()).removeItemProperty(propertyId);
+ }
+
+ // Sends a change event
+ fireContainerPropertySetChange();
+
+ return true;
+ }
+
+}
diff --git a/server/src/com/vaadin/data/util/AbstractContainer.java b/server/src/com/vaadin/data/util/AbstractContainer.java
new file mode 100644
index 0000000000..7d96c2d757
--- /dev/null
+++ b/server/src/com/vaadin/data/util/AbstractContainer.java
@@ -0,0 +1,251 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util;
+
+import java.io.Serializable;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.EventObject;
+import java.util.LinkedList;
+
+import com.vaadin.data.Container;
+
+/**
+ * Abstract container class that manages event listeners and sending events to
+ * them ({@link PropertySetChangeNotifier}, {@link ItemSetChangeNotifier}).
+ *
+ * Note that this class provides the internal implementations for both types of
+ * events and notifiers as protected methods, but does not implement the
+ * {@link PropertySetChangeNotifier} and {@link ItemSetChangeNotifier}
+ * interfaces directly. This way, subclasses can choose not to implement them.
+ * Subclasses implementing those interfaces should also override the
+ * corresponding {@link #addListener()} and {@link #removeListener()} methods to
+ * make them public.
+ *
+ * @since 6.6
+ */
+public abstract class AbstractContainer implements Container {
+
+ /**
+ * List of all Property set change event listeners.
+ */
+ private Collection<Container.PropertySetChangeListener> propertySetChangeListeners = null;
+
+ /**
+ * List of all container Item set change event listeners.
+ */
+ private Collection<Container.ItemSetChangeListener> itemSetChangeListeners = null;
+
+ /**
+ * An <code>event</code> object specifying the container whose Property set
+ * has changed.
+ *
+ * This class does not provide information about which properties were
+ * concerned by the change, but subclasses can provide additional
+ * information about the changes.
+ */
+ protected static class BasePropertySetChangeEvent extends EventObject
+ implements Container.PropertySetChangeEvent, Serializable {
+
+ protected BasePropertySetChangeEvent(Container source) {
+ super(source);
+ }
+
+ @Override
+ public Container getContainer() {
+ return (Container) getSource();
+ }
+ }
+
+ /**
+ * An <code>event</code> object specifying the container whose Item set has
+ * changed.
+ *
+ * This class does not provide information about the exact changes
+ * performed, but subclasses can add provide additional information about
+ * the changes.
+ */
+ protected static class BaseItemSetChangeEvent extends EventObject implements
+ Container.ItemSetChangeEvent, Serializable {
+
+ protected BaseItemSetChangeEvent(Container source) {
+ super(source);
+ }
+
+ @Override
+ public Container getContainer() {
+ return (Container) getSource();
+ }
+ }
+
+ // PropertySetChangeNotifier
+
+ /**
+ * Implementation of the corresponding method in
+ * {@link PropertySetChangeNotifier}, override with the corresponding public
+ * method and implement the interface to use this.
+ *
+ * @see PropertySetChangeNotifier#addListener(com.vaadin.data.Container.PropertySetChangeListener)
+ */
+ protected void addListener(Container.PropertySetChangeListener listener) {
+ if (getPropertySetChangeListeners() == null) {
+ setPropertySetChangeListeners(new LinkedList<Container.PropertySetChangeListener>());
+ }
+ getPropertySetChangeListeners().add(listener);
+ }
+
+ /**
+ * Implementation of the corresponding method in
+ * {@link PropertySetChangeNotifier}, override with the corresponding public
+ * method and implement the interface to use this.
+ *
+ * @see PropertySetChangeNotifier#removeListener(com.vaadin.data.Container.
+ * PropertySetChangeListener)
+ */
+ protected void removeListener(Container.PropertySetChangeListener listener) {
+ if (getPropertySetChangeListeners() != null) {
+ getPropertySetChangeListeners().remove(listener);
+ }
+ }
+
+ // ItemSetChangeNotifier
+
+ /**
+ * Implementation of the corresponding method in
+ * {@link ItemSetChangeNotifier}, override with the corresponding public
+ * method and implement the interface to use this.
+ *
+ * @see ItemSetChangeNotifier#addListener(com.vaadin.data.Container.ItemSetChangeListener)
+ */
+ protected void addListener(Container.ItemSetChangeListener listener) {
+ if (getItemSetChangeListeners() == null) {
+ setItemSetChangeListeners(new LinkedList<Container.ItemSetChangeListener>());
+ }
+ getItemSetChangeListeners().add(listener);
+ }
+
+ /**
+ * Implementation of the corresponding method in
+ * {@link ItemSetChangeNotifier}, override with the corresponding public
+ * method and implement the interface to use this.
+ *
+ * @see ItemSetChangeNotifier#removeListener(com.vaadin.data.Container.ItemSetChangeListener)
+ */
+ protected void removeListener(Container.ItemSetChangeListener listener) {
+ if (getItemSetChangeListeners() != null) {
+ getItemSetChangeListeners().remove(listener);
+ }
+ }
+
+ /**
+ * Sends a simple Property set change event to all interested listeners.
+ */
+ protected void fireContainerPropertySetChange() {
+ fireContainerPropertySetChange(new BasePropertySetChangeEvent(this));
+ }
+
+ /**
+ * Sends a Property set change event to all interested listeners.
+ *
+ * Use {@link #fireContainerPropertySetChange()} instead of this method
+ * unless additional information about the exact changes is available and
+ * should be included in the event.
+ *
+ * @param event
+ * the property change event to send, optionally with additional
+ * information
+ */
+ protected void fireContainerPropertySetChange(
+ Container.PropertySetChangeEvent event) {
+ if (getPropertySetChangeListeners() != null) {
+ final Object[] l = getPropertySetChangeListeners().toArray();
+ for (int i = 0; i < l.length; i++) {
+ ((Container.PropertySetChangeListener) l[i])
+ .containerPropertySetChange(event);
+ }
+ }
+ }
+
+ /**
+ * Sends a simple Item set change event to all interested listeners,
+ * indicating that anything in the contents may have changed (items added,
+ * removed etc.).
+ */
+ protected void fireItemSetChange() {
+ fireItemSetChange(new BaseItemSetChangeEvent(this));
+ }
+
+ /**
+ * Sends an Item set change event to all registered interested listeners.
+ *
+ * @param event
+ * the item set change event to send, optionally with additional
+ * information
+ */
+ protected void fireItemSetChange(ItemSetChangeEvent event) {
+ if (getItemSetChangeListeners() != null) {
+ final Object[] l = getItemSetChangeListeners().toArray();
+ for (int i = 0; i < l.length; i++) {
+ ((Container.ItemSetChangeListener) l[i])
+ .containerItemSetChange(event);
+ }
+ }
+ }
+
+ /**
+ * Sets the property set change listener collection. For internal use only.
+ *
+ * @param propertySetChangeListeners
+ */
+ protected void setPropertySetChangeListeners(
+ Collection<Container.PropertySetChangeListener> propertySetChangeListeners) {
+ this.propertySetChangeListeners = propertySetChangeListeners;
+ }
+
+ /**
+ * Returns the property set change listener collection. For internal use
+ * only.
+ */
+ protected Collection<Container.PropertySetChangeListener> getPropertySetChangeListeners() {
+ return propertySetChangeListeners;
+ }
+
+ /**
+ * Sets the item set change listener collection. For internal use only.
+ *
+ * @param itemSetChangeListeners
+ */
+ protected void setItemSetChangeListeners(
+ Collection<Container.ItemSetChangeListener> itemSetChangeListeners) {
+ this.itemSetChangeListeners = itemSetChangeListeners;
+ }
+
+ /**
+ * Returns the item set change listener collection. For internal use only.
+ */
+ protected Collection<Container.ItemSetChangeListener> getItemSetChangeListeners() {
+ return itemSetChangeListeners;
+ }
+
+ public Collection<?> getListeners(Class<?> eventType) {
+ if (Container.PropertySetChangeEvent.class.isAssignableFrom(eventType)) {
+ if (propertySetChangeListeners == null) {
+ return Collections.EMPTY_LIST;
+ } else {
+ return Collections
+ .unmodifiableCollection(propertySetChangeListeners);
+ }
+ } else if (Container.ItemSetChangeEvent.class
+ .isAssignableFrom(eventType)) {
+ if (itemSetChangeListeners == null) {
+ return Collections.EMPTY_LIST;
+ } else {
+ return Collections
+ .unmodifiableCollection(itemSetChangeListeners);
+ }
+ }
+
+ return Collections.EMPTY_LIST;
+ }
+}
diff --git a/server/src/com/vaadin/data/util/AbstractInMemoryContainer.java b/server/src/com/vaadin/data/util/AbstractInMemoryContainer.java
new file mode 100644
index 0000000000..b7832756f2
--- /dev/null
+++ b/server/src/com/vaadin/data/util/AbstractInMemoryContainer.java
@@ -0,0 +1,941 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Set;
+
+import com.vaadin.data.Container;
+import com.vaadin.data.Container.ItemSetChangeNotifier;
+import com.vaadin.data.Item;
+import com.vaadin.data.util.filter.SimpleStringFilter;
+import com.vaadin.data.util.filter.UnsupportedFilterException;
+
+/**
+ * Abstract {@link Container} class that handles common functionality for
+ * in-memory containers. Concrete in-memory container classes can either inherit
+ * this class, inherit {@link AbstractContainer}, or implement the
+ * {@link Container} interface directly.
+ *
+ * Adding and removing items (if desired) must be implemented in subclasses by
+ * overriding the appropriate add*Item() and remove*Item() and removeAllItems()
+ * methods, calling the corresponding
+ * {@link #internalAddItemAfter(Object, Object, Item)},
+ * {@link #internalAddItemAt(int, Object, Item)},
+ * {@link #internalAddItemAtEnd(Object, Item, boolean)},
+ * {@link #internalRemoveItem(Object)} and {@link #internalRemoveAllItems()}
+ * methods.
+ *
+ * By default, adding and removing container properties is not supported, and
+ * subclasses need to implement {@link #getContainerPropertyIds()}. Optionally,
+ * subclasses can override {@link #addContainerProperty(Object, Class, Object)}
+ * and {@link #removeContainerProperty(Object)} to implement them.
+ *
+ * Features:
+ * <ul>
+ * <li> {@link Container.Ordered}
+ * <li> {@link Container.Indexed}
+ * <li> {@link Filterable} and {@link SimpleFilterable} (internal implementation,
+ * does not implement the interface directly)
+ * <li> {@link Sortable} (internal implementation, does not implement the
+ * interface directly)
+ * </ul>
+ *
+ * To implement {@link Sortable}, subclasses need to implement
+ * {@link #getSortablePropertyIds()} and call the superclass method
+ * {@link #sortContainer(Object[], boolean[])} in the method
+ * <code>sort(Object[], boolean[])</code>.
+ *
+ * To implement {@link Filterable}, subclasses need to implement the methods
+ * {@link Filterable#addContainerFilter(com.vaadin.data.Container.Filter)}
+ * (calling {@link #addFilter(Filter)}),
+ * {@link Filterable#removeAllContainerFilters()} (calling
+ * {@link #removeAllFilters()}) and
+ * {@link Filterable#removeContainerFilter(com.vaadin.data.Container.Filter)}
+ * (calling {@link #removeFilter(com.vaadin.data.Container.Filter)}).
+ *
+ * To implement {@link SimpleFilterable}, subclasses also need to implement the
+ * methods
+ * {@link SimpleFilterable#addContainerFilter(Object, String, boolean, boolean)}
+ * and {@link SimpleFilterable#removeContainerFilters(Object)} calling
+ * {@link #addFilter(com.vaadin.data.Container.Filter)} and
+ * {@link #removeFilters(Object)} respectively.
+ *
+ * @param <ITEMIDTYPE>
+ * the class of item identifiers in the container, use Object if can
+ * be any class
+ * @param <PROPERTYIDCLASS>
+ * the class of property identifiers for the items in the container,
+ * use Object if can be any class
+ * @param <ITEMCLASS>
+ * the (base) class of the Item instances in the container, use
+ * {@link Item} if unknown
+ *
+ * @since 6.6
+ */
+public abstract class AbstractInMemoryContainer<ITEMIDTYPE, PROPERTYIDCLASS, ITEMCLASS extends Item>
+ extends AbstractContainer implements ItemSetChangeNotifier,
+ Container.Indexed {
+
+ /**
+ * An ordered {@link List} of all item identifiers in the container,
+ * including those that have been filtered out.
+ *
+ * Must not be null.
+ */
+ private List<ITEMIDTYPE> allItemIds;
+
+ /**
+ * An ordered {@link List} of item identifiers in the container after
+ * filtering, excluding those that have been filtered out.
+ *
+ * This is what the external API of the {@link Container} interface and its
+ * subinterfaces shows (e.g. {@link #size()}, {@link #nextItemId(Object)}).
+ *
+ * If null, the full item id list is used instead.
+ */
+ private List<ITEMIDTYPE> filteredItemIds;
+
+ /**
+ * Filters that are applied to the container to limit the items visible in
+ * it
+ */
+ private Set<Filter> filters = new HashSet<Filter>();
+
+ /**
+ * The item sorter which is used for sorting the container.
+ */
+ private ItemSorter itemSorter = new DefaultItemSorter();
+
+ // Constructors
+
+ /**
+ * Constructor for an abstract in-memory container.
+ */
+ protected AbstractInMemoryContainer() {
+ setAllItemIds(new ListSet<ITEMIDTYPE>());
+ }
+
+ // Container interface methods with more specific return class
+
+ // default implementation, can be overridden
+ @Override
+ public ITEMCLASS getItem(Object itemId) {
+ if (containsId(itemId)) {
+ return getUnfilteredItem(itemId);
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Get an item even if filtered out.
+ *
+ * For internal use only.
+ *
+ * @param itemId
+ * @return
+ */
+ protected abstract ITEMCLASS getUnfilteredItem(Object itemId);
+
+ // cannot override getContainerPropertyIds() and getItemIds(): if subclass
+ // uses Object as ITEMIDCLASS or PROPERTYIDCLASS, Collection<Object> cannot
+ // be cast to Collection<MyInterface>
+
+ // public abstract Collection<PROPERTYIDCLASS> getContainerPropertyIds();
+ // public abstract Collection<ITEMIDCLASS> getItemIds();
+
+ // Container interface method implementations
+
+ @Override
+ public int size() {
+ return getVisibleItemIds().size();
+ }
+
+ @Override
+ public boolean containsId(Object itemId) {
+ // only look at visible items after filtering
+ if (itemId == null) {
+ return false;
+ } else {
+ return getVisibleItemIds().contains(itemId);
+ }
+ }
+
+ @Override
+ public List<?> getItemIds() {
+ return Collections.unmodifiableList(getVisibleItemIds());
+ }
+
+ // Container.Ordered
+
+ @Override
+ public ITEMIDTYPE nextItemId(Object itemId) {
+ int index = indexOfId(itemId);
+ if (index >= 0 && index < size() - 1) {
+ return getIdByIndex(index + 1);
+ } else {
+ // out of bounds
+ return null;
+ }
+ }
+
+ @Override
+ public ITEMIDTYPE prevItemId(Object itemId) {
+ int index = indexOfId(itemId);
+ if (index > 0) {
+ return getIdByIndex(index - 1);
+ } else {
+ // out of bounds
+ return null;
+ }
+ }
+
+ @Override
+ public ITEMIDTYPE firstItemId() {
+ if (size() > 0) {
+ return getIdByIndex(0);
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ public ITEMIDTYPE lastItemId() {
+ if (size() > 0) {
+ return getIdByIndex(size() - 1);
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ public boolean isFirstId(Object itemId) {
+ if (itemId == null) {
+ return false;
+ }
+ return itemId.equals(firstItemId());
+ }
+
+ @Override
+ public boolean isLastId(Object itemId) {
+ if (itemId == null) {
+ return false;
+ }
+ return itemId.equals(lastItemId());
+ }
+
+ // Container.Indexed
+
+ @Override
+ public ITEMIDTYPE getIdByIndex(int index) {
+ return getVisibleItemIds().get(index);
+ }
+
+ @Override
+ public int indexOfId(Object itemId) {
+ return getVisibleItemIds().indexOf(itemId);
+ }
+
+ // methods that are unsupported by default, override to support
+
+ @Override
+ public Object addItemAt(int index) throws UnsupportedOperationException {
+ throw new UnsupportedOperationException(
+ "Adding items not supported. Override the relevant addItem*() methods if required as specified in AbstractInMemoryContainer javadoc.");
+ }
+
+ @Override
+ public Item addItemAt(int index, Object newItemId)
+ throws UnsupportedOperationException {
+ throw new UnsupportedOperationException(
+ "Adding items not supported. Override the relevant addItem*() methods if required as specified in AbstractInMemoryContainer javadoc.");
+ }
+
+ @Override
+ public Object addItemAfter(Object previousItemId)
+ throws UnsupportedOperationException {
+ throw new UnsupportedOperationException(
+ "Adding items not supported. Override the relevant addItem*() methods if required as specified in AbstractInMemoryContainer javadoc.");
+ }
+
+ @Override
+ public Item addItemAfter(Object previousItemId, Object newItemId)
+ throws UnsupportedOperationException {
+ throw new UnsupportedOperationException(
+ "Adding items not supported. Override the relevant addItem*() methods if required as specified in AbstractInMemoryContainer javadoc.");
+ }
+
+ @Override
+ public Item addItem(Object itemId) throws UnsupportedOperationException {
+ throw new UnsupportedOperationException(
+ "Adding items not supported. Override the relevant addItem*() methods if required as specified in AbstractInMemoryContainer javadoc.");
+ }
+
+ @Override
+ public Object addItem() throws UnsupportedOperationException {
+ throw new UnsupportedOperationException(
+ "Adding items not supported. Override the relevant addItem*() methods if required as specified in AbstractInMemoryContainer javadoc.");
+ }
+
+ @Override
+ public boolean removeItem(Object itemId)
+ throws UnsupportedOperationException {
+ throw new UnsupportedOperationException(
+ "Removing items not supported. Override the removeItem() method if required as specified in AbstractInMemoryContainer javadoc.");
+ }
+
+ @Override
+ public boolean removeAllItems() throws UnsupportedOperationException {
+ throw new UnsupportedOperationException(
+ "Removing items not supported. Override the removeAllItems() method if required as specified in AbstractInMemoryContainer javadoc.");
+ }
+
+ @Override
+ public boolean addContainerProperty(Object propertyId, Class<?> type,
+ Object defaultValue) throws UnsupportedOperationException {
+ throw new UnsupportedOperationException(
+ "Adding container properties not supported. Override the addContainerProperty() method if required.");
+ }
+
+ @Override
+ public boolean removeContainerProperty(Object propertyId)
+ throws UnsupportedOperationException {
+ throw new UnsupportedOperationException(
+ "Removing container properties not supported. Override the addContainerProperty() method if required.");
+ }
+
+ // ItemSetChangeNotifier
+
+ @Override
+ public void addListener(Container.ItemSetChangeListener listener) {
+ super.addListener(listener);
+ }
+
+ @Override
+ public void removeListener(Container.ItemSetChangeListener listener) {
+ super.removeListener(listener);
+ }
+
+ // internal methods
+
+ // Filtering support
+
+ /**
+ * Filter the view to recreate the visible item list from the unfiltered
+ * items, and send a notification if the set of visible items changed in any
+ * way.
+ */
+ protected void filterAll() {
+ if (doFilterContainer(!getFilters().isEmpty())) {
+ fireItemSetChange();
+ }
+ }
+
+ /**
+ * Filters the data in the container and updates internal data structures.
+ * This method should reset any internal data structures and then repopulate
+ * them so {@link #getItemIds()} and other methods only return the filtered
+ * items.
+ *
+ * @param hasFilters
+ * true if filters has been set for the container, false
+ * otherwise
+ * @return true if the item set has changed as a result of the filtering
+ */
+ protected boolean doFilterContainer(boolean hasFilters) {
+ if (!hasFilters) {
+ boolean changed = getAllItemIds().size() != getVisibleItemIds()
+ .size();
+ setFilteredItemIds(null);
+ return changed;
+ }
+
+ // Reset filtered list
+ List<ITEMIDTYPE> originalFilteredItemIds = getFilteredItemIds();
+ boolean wasUnfiltered = false;
+ if (originalFilteredItemIds == null) {
+ originalFilteredItemIds = Collections.emptyList();
+ wasUnfiltered = true;
+ }
+ setFilteredItemIds(new ListSet<ITEMIDTYPE>());
+
+ // Filter
+ boolean equal = true;
+ Iterator<ITEMIDTYPE> origIt = originalFilteredItemIds.iterator();
+ for (final Iterator<ITEMIDTYPE> i = getAllItemIds().iterator(); i
+ .hasNext();) {
+ final ITEMIDTYPE id = i.next();
+ if (passesFilters(id)) {
+ // filtered list comes from the full list, can use ==
+ equal = equal && origIt.hasNext() && origIt.next() == id;
+ getFilteredItemIds().add(id);
+ }
+ }
+
+ return (wasUnfiltered && !getAllItemIds().isEmpty()) || !equal
+ || origIt.hasNext();
+ }
+
+ /**
+ * Checks if the given itemId passes the filters set for the container. The
+ * caller should make sure the itemId exists in the container. For
+ * non-existing itemIds the behavior is undefined.
+ *
+ * @param itemId
+ * An itemId that exists in the container.
+ * @return true if the itemId passes all filters or no filters are set,
+ * false otherwise.
+ */
+ protected boolean passesFilters(Object itemId) {
+ ITEMCLASS item = getUnfilteredItem(itemId);
+ if (getFilters().isEmpty()) {
+ return true;
+ }
+ final Iterator<Filter> i = getFilters().iterator();
+ while (i.hasNext()) {
+ final Filter f = i.next();
+ if (!f.passesFilter(itemId, item)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Adds a container filter and re-filter the view.
+ *
+ * The filter must implement Filter and its sub-filters (if any) must also
+ * be in-memory filterable.
+ *
+ * This can be used to implement
+ * {@link Filterable#addContainerFilter(com.vaadin.data.Container.Filter)}
+ * and optionally also
+ * {@link SimpleFilterable#addContainerFilter(Object, String, boolean, boolean)}
+ * (with {@link SimpleStringFilter}).
+ *
+ * Note that in some cases, incompatible filters cannot be detected when
+ * added and an {@link UnsupportedFilterException} may occur when performing
+ * filtering.
+ *
+ * @throws UnsupportedFilterException
+ * if the filter is detected as not supported by the container
+ */
+ protected void addFilter(Filter filter) throws UnsupportedFilterException {
+ getFilters().add(filter);
+ filterAll();
+ }
+
+ /**
+ * Remove a specific container filter and re-filter the view (if necessary).
+ *
+ * This can be used to implement
+ * {@link Filterable#removeContainerFilter(com.vaadin.data.Container.Filter)}
+ * .
+ */
+ protected void removeFilter(Filter filter) {
+ for (Iterator<Filter> iterator = getFilters().iterator(); iterator
+ .hasNext();) {
+ Filter f = iterator.next();
+ if (f.equals(filter)) {
+ iterator.remove();
+ filterAll();
+ return;
+ }
+ }
+ }
+
+ /**
+ * Remove all container filters for all properties and re-filter the view.
+ *
+ * This can be used to implement
+ * {@link Filterable#removeAllContainerFilters()}.
+ */
+ protected void removeAllFilters() {
+ if (getFilters().isEmpty()) {
+ return;
+ }
+ getFilters().clear();
+ filterAll();
+ }
+
+ /**
+ * Checks if there is a filter that applies to a given property.
+ *
+ * @param propertyId
+ * @return true if there is an active filter for the property
+ */
+ protected boolean isPropertyFiltered(Object propertyId) {
+ if (getFilters().isEmpty() || propertyId == null) {
+ return false;
+ }
+ final Iterator<Filter> i = getFilters().iterator();
+ while (i.hasNext()) {
+ final Filter f = i.next();
+ if (f.appliesToProperty(propertyId)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Remove all container filters for a given property identifier and
+ * re-filter the view. This also removes filters applying to multiple
+ * properties including the one identified by propertyId.
+ *
+ * This can be used to implement
+ * {@link Filterable#removeContainerFilters(Object)}.
+ *
+ * @param propertyId
+ * @return Collection<Filter> removed filters
+ */
+ protected Collection<Filter> removeFilters(Object propertyId) {
+ if (getFilters().isEmpty() || propertyId == null) {
+ return Collections.emptyList();
+ }
+ List<Filter> removedFilters = new LinkedList<Filter>();
+ for (Iterator<Filter> iterator = getFilters().iterator(); iterator
+ .hasNext();) {
+ Filter f = iterator.next();
+ if (f.appliesToProperty(propertyId)) {
+ removedFilters.add(f);
+ iterator.remove();
+ }
+ }
+ if (!removedFilters.isEmpty()) {
+ filterAll();
+ return removedFilters;
+ }
+ return Collections.emptyList();
+ }
+
+ // sorting
+
+ /**
+ * Returns the ItemSorter used for comparing items in a sort. See
+ * {@link #setItemSorter(ItemSorter)} for more information.
+ *
+ * @return The ItemSorter used for comparing two items in a sort.
+ */
+ protected ItemSorter getItemSorter() {
+ return itemSorter;
+ }
+
+ /**
+ * Sets the ItemSorter used for comparing items in a sort. The
+ * {@link ItemSorter#compare(Object, Object)} method is called with item ids
+ * to perform the sorting. A default ItemSorter is used if this is not
+ * explicitly set.
+ *
+ * @param itemSorter
+ * The ItemSorter used for comparing two items in a sort (not
+ * null).
+ */
+ protected void setItemSorter(ItemSorter itemSorter) {
+ this.itemSorter = itemSorter;
+ }
+
+ /**
+ * Sort base implementation to be used to implement {@link Sortable}.
+ *
+ * Subclasses should call this from a public
+ * {@link #sort(Object[], boolean[])} method when implementing Sortable.
+ *
+ * @see com.vaadin.data.Container.Sortable#sort(java.lang.Object[],
+ * boolean[])
+ */
+ protected void sortContainer(Object[] propertyId, boolean[] ascending) {
+ if (!(this instanceof Sortable)) {
+ throw new UnsupportedOperationException(
+ "Cannot sort a Container that does not implement Sortable");
+ }
+
+ // Set up the item sorter for the sort operation
+ getItemSorter().setSortProperties((Sortable) this, propertyId,
+ ascending);
+
+ // Perform the actual sort
+ doSort();
+
+ // Post sort updates
+ if (isFiltered()) {
+ filterAll();
+ } else {
+ fireItemSetChange();
+ }
+
+ }
+
+ /**
+ * Perform the sorting of the data structures in the container. This is
+ * invoked when the <code>itemSorter</code> has been prepared for the sort
+ * operation. Typically this method calls
+ * <code>Collections.sort(aCollection, getItemSorter())</code> on all arrays
+ * (containing item ids) that need to be sorted.
+ *
+ */
+ protected void doSort() {
+ Collections.sort(getAllItemIds(), getItemSorter());
+ }
+
+ /**
+ * Returns the sortable property identifiers for the container. Can be used
+ * to implement {@link Sortable#getSortableContainerPropertyIds()}.
+ */
+ protected Collection<?> getSortablePropertyIds() {
+ LinkedList<Object> sortables = new LinkedList<Object>();
+ for (Object propertyId : getContainerPropertyIds()) {
+ Class<?> propertyType = getType(propertyId);
+ if (Comparable.class.isAssignableFrom(propertyType)
+ || propertyType.isPrimitive()) {
+ sortables.add(propertyId);
+ }
+ }
+ return sortables;
+ }
+
+ // removing items
+
+ /**
+ * Removes all items from the internal data structures of this class. This
+ * can be used to implement {@link #removeAllItems()} in subclasses.
+ *
+ * No notification is sent, the caller has to fire a suitable item set
+ * change notification.
+ */
+ protected void internalRemoveAllItems() {
+ // Removes all Items
+ getAllItemIds().clear();
+ if (isFiltered()) {
+ getFilteredItemIds().clear();
+ }
+ }
+
+ /**
+ * Removes a single item from the internal data structures of this class.
+ * This can be used to implement {@link #removeItem(Object)} in subclasses.
+ *
+ * No notification is sent, the caller has to fire a suitable item set
+ * change notification.
+ *
+ * @param itemId
+ * the identifier of the item to remove
+ * @return true if an item was successfully removed, false if failed to
+ * remove or no such item
+ */
+ protected boolean internalRemoveItem(Object itemId) {
+ if (itemId == null) {
+ return false;
+ }
+
+ boolean result = getAllItemIds().remove(itemId);
+ if (result && isFiltered()) {
+ getFilteredItemIds().remove(itemId);
+ }
+
+ return result;
+ }
+
+ // adding items
+
+ /**
+ * Adds the bean to all internal data structures at the given position.
+ * Fails if an item with itemId is already in the container. Returns a the
+ * item if it was added successfully, null otherwise.
+ *
+ * <p>
+ * Caller should initiate filtering after calling this method.
+ * </p>
+ *
+ * For internal use only - subclasses should use
+ * {@link #internalAddItemAtEnd(Object, Item, boolean)},
+ * {@link #internalAddItemAt(int, Object, Item, boolean)} and
+ * {@link #internalAddItemAfter(Object, Object, Item, boolean)} instead.
+ *
+ * @param position
+ * The position at which the item should be inserted in the
+ * unfiltered collection of items
+ * @param itemId
+ * The item identifier for the item to insert
+ * @param item
+ * The item to insert
+ *
+ * @return ITEMCLASS if the item was added successfully, null otherwise
+ */
+ private ITEMCLASS internalAddAt(int position, ITEMIDTYPE itemId,
+ ITEMCLASS item) {
+ if (position < 0 || position > getAllItemIds().size() || itemId == null
+ || item == null) {
+ return null;
+ }
+ // Make sure that the item has not been added previously
+ if (getAllItemIds().contains(itemId)) {
+ return null;
+ }
+
+ // "filteredList" will be updated in filterAll() which should be invoked
+ // by the caller after calling this method.
+ getAllItemIds().add(position, itemId);
+ registerNewItem(position, itemId, item);
+
+ return item;
+ }
+
+ /**
+ * Add an item at the end of the container, and perform filtering if
+ * necessary. An event is fired if the filtered view changes.
+ *
+ * @param newItemId
+ * @param item
+ * new item to add
+ * @param filter
+ * true to perform filtering and send event after adding the
+ * item, false to skip these operations for batch inserts - if
+ * false, caller needs to make sure these operations are
+ * performed at the end of the batch
+ * @return item added or null if no item was added
+ */
+ protected ITEMCLASS internalAddItemAtEnd(ITEMIDTYPE newItemId,
+ ITEMCLASS item, boolean filter) {
+ ITEMCLASS newItem = internalAddAt(getAllItemIds().size(), newItemId,
+ item);
+ if (newItem != null && filter) {
+ // TODO filter only this item, use fireItemAdded()
+ filterAll();
+ if (!isFiltered()) {
+ // TODO hack: does not detect change in filterAll() in this case
+ fireItemAdded(indexOfId(newItemId), newItemId, item);
+ }
+ }
+ return newItem;
+ }
+
+ /**
+ * Add an item after a given (visible) item, and perform filtering. An event
+ * is fired if the filtered view changes.
+ *
+ * The new item is added at the beginning if previousItemId is null.
+ *
+ * @param previousItemId
+ * item id of a visible item after which to add the new item, or
+ * null to add at the beginning
+ * @param newItemId
+ * @param item
+ * new item to add
+ * @param filter
+ * true to perform filtering and send event after adding the
+ * item, false to skip these operations for batch inserts - if
+ * false, caller needs to make sure these operations are
+ * performed at the end of the batch
+ * @return item added or null if no item was added
+ */
+ protected ITEMCLASS internalAddItemAfter(ITEMIDTYPE previousItemId,
+ ITEMIDTYPE newItemId, ITEMCLASS item, boolean filter) {
+ // only add if the previous item is visible
+ ITEMCLASS newItem = null;
+ if (previousItemId == null) {
+ newItem = internalAddAt(0, newItemId, item);
+ } else if (containsId(previousItemId)) {
+ newItem = internalAddAt(
+ getAllItemIds().indexOf(previousItemId) + 1, newItemId,
+ item);
+ }
+ if (newItem != null && filter) {
+ // TODO filter only this item, use fireItemAdded()
+ filterAll();
+ if (!isFiltered()) {
+ // TODO hack: does not detect change in filterAll() in this case
+ fireItemAdded(indexOfId(newItemId), newItemId, item);
+ }
+ }
+ return newItem;
+ }
+
+ /**
+ * Add an item at a given (visible after filtering) item index, and perform
+ * filtering. An event is fired if the filtered view changes.
+ *
+ * @param index
+ * position where to add the item (visible/view index)
+ * @param newItemId
+ * @param item
+ * new item to add
+ * @param filter
+ * true to perform filtering and send event after adding the
+ * item, false to skip these operations for batch inserts - if
+ * false, caller needs to make sure these operations are
+ * performed at the end of the batch
+ * @return item added or null if no item was added
+ */
+ protected ITEMCLASS internalAddItemAt(int index, ITEMIDTYPE newItemId,
+ ITEMCLASS item, boolean filter) {
+ if (index < 0 || index > size()) {
+ return null;
+ } else if (index == 0) {
+ // add before any item, visible or not
+ return internalAddItemAfter(null, newItemId, item, filter);
+ } else {
+ // if index==size(), adds immediately after last visible item
+ return internalAddItemAfter(getIdByIndex(index - 1), newItemId,
+ item, filter);
+ }
+ }
+
+ /**
+ * Registers a new item as having been added to the container. This can
+ * involve storing the item or any relevant information about it in internal
+ * container-specific collections if necessary, as well as registering
+ * listeners etc.
+ *
+ * The full identifier list in {@link AbstractInMemoryContainer} has already
+ * been updated to reflect the new item when this method is called.
+ *
+ * @param position
+ * @param itemId
+ * @param item
+ */
+ protected void registerNewItem(int position, ITEMIDTYPE itemId,
+ ITEMCLASS item) {
+ }
+
+ // item set change notifications
+
+ /**
+ * Notify item set change listeners that an item has been added to the
+ * container.
+ *
+ * Unless subclasses specify otherwise, the default notification indicates a
+ * full refresh.
+ *
+ * @param postion
+ * position of the added item in the view (if visible)
+ * @param itemId
+ * id of the added item
+ * @param item
+ * the added item
+ */
+ protected void fireItemAdded(int position, ITEMIDTYPE itemId, ITEMCLASS item) {
+ fireItemSetChange();
+ }
+
+ /**
+ * Notify item set change listeners that an item has been removed from the
+ * container.
+ *
+ * Unless subclasses specify otherwise, the default notification indicates a
+ * full refresh.
+ *
+ * @param postion
+ * position of the removed item in the view prior to removal (if
+ * was visible)
+ * @param itemId
+ * id of the removed item, of type {@link Object} to satisfy
+ * {@link Container#removeItem(Object)} API
+ */
+ protected void fireItemRemoved(int position, Object itemId) {
+ fireItemSetChange();
+ }
+
+ // visible and filtered item identifier lists
+
+ /**
+ * Returns the internal list of visible item identifiers after filtering.
+ *
+ * For internal use only.
+ */
+ protected List<ITEMIDTYPE> getVisibleItemIds() {
+ if (isFiltered()) {
+ return getFilteredItemIds();
+ } else {
+ return getAllItemIds();
+ }
+ }
+
+ /**
+ * Returns true is the container has active filters.
+ *
+ * @return true if the container is currently filtered
+ */
+ protected boolean isFiltered() {
+ return filteredItemIds != null;
+ }
+
+ /**
+ * Internal helper method to set the internal list of filtered item
+ * identifiers. Should not be used outside this class except for
+ * implementing clone(), may disappear from future versions.
+ *
+ * @param filteredItemIds
+ */
+ @Deprecated
+ protected void setFilteredItemIds(List<ITEMIDTYPE> filteredItemIds) {
+ this.filteredItemIds = filteredItemIds;
+ }
+
+ /**
+ * Internal helper method to get the internal list of filtered item
+ * identifiers. Should not be used outside this class except for
+ * implementing clone(), may disappear from future versions - use
+ * {@link #getVisibleItemIds()} in other contexts.
+ *
+ * @return List<ITEMIDTYPE>
+ */
+ protected List<ITEMIDTYPE> getFilteredItemIds() {
+ return filteredItemIds;
+ }
+
+ /**
+ * Internal helper method to set the internal list of all item identifiers.
+ * Should not be used outside this class except for implementing clone(),
+ * may disappear from future versions.
+ *
+ * @param allItemIds
+ */
+ @Deprecated
+ protected void setAllItemIds(List<ITEMIDTYPE> allItemIds) {
+ this.allItemIds = allItemIds;
+ }
+
+ /**
+ * Internal helper method to get the internal list of all item identifiers.
+ * Avoid using this method outside this class, may disappear in future
+ * versions.
+ *
+ * @return List<ITEMIDTYPE>
+ */
+ protected List<ITEMIDTYPE> getAllItemIds() {
+ return allItemIds;
+ }
+
+ /**
+ * Set the internal collection of filters without performing filtering.
+ *
+ * This method is mostly for internal use, use
+ * {@link #addFilter(com.vaadin.data.Container.Filter)} and
+ * <code>remove*Filter*</code> (which also re-filter the container) instead
+ * when possible.
+ *
+ * @param filters
+ */
+ protected void setFilters(Set<Filter> filters) {
+ this.filters = filters;
+ }
+
+ /**
+ * Returns the internal collection of filters. The returned collection
+ * should not be modified by callers outside this class.
+ *
+ * @return Set<Filter>
+ */
+ protected Set<Filter> getFilters() {
+ return filters;
+ }
+
+}
diff --git a/server/src/com/vaadin/data/util/AbstractProperty.java b/server/src/com/vaadin/data/util/AbstractProperty.java
new file mode 100644
index 0000000000..373a8dfd58
--- /dev/null
+++ b/server/src/com/vaadin/data/util/AbstractProperty.java
@@ -0,0 +1,226 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedList;
+
+import com.vaadin.data.Property;
+
+/**
+ * Abstract base class for {@link Property} implementations.
+ *
+ * Handles listener management for {@link ValueChangeListener}s and
+ * {@link ReadOnlyStatusChangeListener}s.
+ *
+ * @since 6.6
+ */
+public abstract class AbstractProperty<T> implements Property<T>,
+ Property.ValueChangeNotifier, Property.ReadOnlyStatusChangeNotifier {
+
+ /**
+ * List of listeners who are interested in the read-only status changes of
+ * the Property
+ */
+ private LinkedList<ReadOnlyStatusChangeListener> readOnlyStatusChangeListeners = null;
+
+ /**
+ * List of listeners who are interested in the value changes of the Property
+ */
+ private LinkedList<ValueChangeListener> valueChangeListeners = null;
+
+ /**
+ * Is the Property read-only?
+ */
+ private boolean readOnly;
+
+ /**
+ * {@inheritDoc}
+ *
+ * Override for additional restrictions on what is considered a read-only
+ * property.
+ */
+ @Override
+ public boolean isReadOnly() {
+ return readOnly;
+ }
+
+ @Override
+ public void setReadOnly(boolean newStatus) {
+ boolean oldStatus = isReadOnly();
+ readOnly = newStatus;
+ if (oldStatus != isReadOnly()) {
+ fireReadOnlyStatusChange();
+ }
+ }
+
+ /**
+ * Returns the value of the <code>Property</code> in human readable textual
+ * format.
+ *
+ * @return String representation of the value stored in the Property
+ * @deprecated use {@link #getValue()} instead and possibly toString on that
+ */
+ @Deprecated
+ @Override
+ public String toString() {
+ throw new UnsupportedOperationException(
+ "Use Property.getValue() instead of " + getClass()
+ + ".toString()");
+ }
+
+ /* Events */
+
+ /**
+ * An <code>Event</code> object specifying the Property whose read-only
+ * status has been changed.
+ */
+ protected static class ReadOnlyStatusChangeEvent extends
+ java.util.EventObject implements Property.ReadOnlyStatusChangeEvent {
+
+ /**
+ * Constructs a new read-only status change event for this object.
+ *
+ * @param source
+ * source object of the event.
+ */
+ protected ReadOnlyStatusChangeEvent(Property source) {
+ super(source);
+ }
+
+ /**
+ * Gets the Property whose read-only state has changed.
+ *
+ * @return source Property of the event.
+ */
+ @Override
+ public Property getProperty() {
+ return (Property) getSource();
+ }
+
+ }
+
+ /**
+ * Registers a new read-only status change listener for this Property.
+ *
+ * @param listener
+ * the new Listener to be registered.
+ */
+ @Override
+ public void addListener(Property.ReadOnlyStatusChangeListener listener) {
+ if (readOnlyStatusChangeListeners == null) {
+ readOnlyStatusChangeListeners = new LinkedList<ReadOnlyStatusChangeListener>();
+ }
+ readOnlyStatusChangeListeners.add(listener);
+ }
+
+ /**
+ * Removes a previously registered read-only status change listener.
+ *
+ * @param listener
+ * the listener to be removed.
+ */
+ @Override
+ public void removeListener(Property.ReadOnlyStatusChangeListener listener) {
+ if (readOnlyStatusChangeListeners != null) {
+ readOnlyStatusChangeListeners.remove(listener);
+ }
+ }
+
+ /**
+ * Sends a read only status change event to all registered listeners.
+ */
+ protected void fireReadOnlyStatusChange() {
+ if (readOnlyStatusChangeListeners != null) {
+ final Object[] l = readOnlyStatusChangeListeners.toArray();
+ final Property.ReadOnlyStatusChangeEvent event = new ReadOnlyStatusChangeEvent(
+ this);
+ for (int i = 0; i < l.length; i++) {
+ ((Property.ReadOnlyStatusChangeListener) l[i])
+ .readOnlyStatusChange(event);
+ }
+ }
+ }
+
+ /**
+ * An <code>Event</code> object specifying the Property whose value has been
+ * changed.
+ */
+ private static class ValueChangeEvent extends java.util.EventObject
+ implements Property.ValueChangeEvent {
+
+ /**
+ * Constructs a new value change event for this object.
+ *
+ * @param source
+ * source object of the event.
+ */
+ protected ValueChangeEvent(Property source) {
+ super(source);
+ }
+
+ /**
+ * Gets the Property whose value has changed.
+ *
+ * @return source Property of the event.
+ */
+ @Override
+ public Property getProperty() {
+ return (Property) getSource();
+ }
+
+ }
+
+ @Override
+ public void addListener(ValueChangeListener listener) {
+ if (valueChangeListeners == null) {
+ valueChangeListeners = new LinkedList<ValueChangeListener>();
+ }
+ valueChangeListeners.add(listener);
+
+ }
+
+ @Override
+ public void removeListener(ValueChangeListener listener) {
+ if (valueChangeListeners != null) {
+ valueChangeListeners.remove(listener);
+ }
+
+ }
+
+ /**
+ * Sends a value change event to all registered listeners.
+ */
+ protected void fireValueChange() {
+ if (valueChangeListeners != null) {
+ final Object[] l = valueChangeListeners.toArray();
+ final Property.ValueChangeEvent event = new ValueChangeEvent(this);
+ for (int i = 0; i < l.length; i++) {
+ ((Property.ValueChangeListener) l[i]).valueChange(event);
+ }
+ }
+ }
+
+ public Collection<?> getListeners(Class<?> eventType) {
+ if (Property.ValueChangeEvent.class.isAssignableFrom(eventType)) {
+ if (valueChangeListeners == null) {
+ return Collections.EMPTY_LIST;
+ } else {
+ return Collections.unmodifiableCollection(valueChangeListeners);
+ }
+ } else if (Property.ReadOnlyStatusChangeEvent.class
+ .isAssignableFrom(eventType)) {
+ if (readOnlyStatusChangeListeners == null) {
+ return Collections.EMPTY_LIST;
+ } else {
+ return Collections
+ .unmodifiableCollection(readOnlyStatusChangeListeners);
+ }
+ }
+
+ return Collections.EMPTY_LIST;
+ }
+
+}
diff --git a/server/src/com/vaadin/data/util/BeanContainer.java b/server/src/com/vaadin/data/util/BeanContainer.java
new file mode 100644
index 0000000000..bc1ee3c39e
--- /dev/null
+++ b/server/src/com/vaadin/data/util/BeanContainer.java
@@ -0,0 +1,168 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util;
+
+import java.util.Collection;
+
+/**
+ * An in-memory container for JavaBeans.
+ *
+ * <p>
+ * The properties of the container are determined automatically by introspecting
+ * the used JavaBean class. Only beans of the same type can be added to the
+ * container.
+ * </p>
+ *
+ * <p>
+ * In BeanContainer (unlike {@link BeanItemContainer}), the item IDs do not have
+ * to be the beans themselves. The container can be used either with explicit
+ * item IDs or the item IDs can be generated when adding beans.
+ * </p>
+ *
+ * <p>
+ * To use explicit item IDs, use the methods {@link #addItem(Object, Object)},
+ * {@link #addItemAfter(Object, Object, Object)} and
+ * {@link #addItemAt(int, Object, Object)}.
+ * </p>
+ *
+ * <p>
+ * If a bean id resolver is set using
+ * {@link #setBeanIdResolver(com.vaadin.data.util.AbstractBeanContainer.BeanIdResolver)}
+ * or {@link #setBeanIdProperty(Object)}, the methods {@link #addBean(Object)},
+ * {@link #addBeanAfter(Object, Object)}, {@link #addBeanAt(int, Object)} and
+ * {@link #addAll(java.util.Collection)} can be used to add items to the
+ * container. If one of these methods is called, the resolver is used to
+ * generate an identifier for the item (must not return null).
+ * </p>
+ *
+ * <p>
+ * Note that explicit item identifiers can also be used when a resolver has been
+ * set by calling the addItem*() methods - the resolver is only used when adding
+ * beans using the addBean*() or {@link #addAll(Collection)} methods.
+ * </p>
+ *
+ * <p>
+ * It is not possible to add additional properties to the container and nested
+ * bean properties are not supported.
+ * </p>
+ *
+ * @param <IDTYPE>
+ * The type of the item identifier
+ * @param <BEANTYPE>
+ * The type of the Bean
+ *
+ * @see AbstractBeanContainer
+ * @see BeanItemContainer
+ *
+ * @since 6.5
+ */
+public class BeanContainer<IDTYPE, BEANTYPE> extends
+ AbstractBeanContainer<IDTYPE, BEANTYPE> {
+
+ public BeanContainer(Class<? super BEANTYPE> type) {
+ super(type);
+ }
+
+ /**
+ * Adds the bean to the Container.
+ *
+ * @see com.vaadin.data.Container#addItem(Object)
+ */
+ @Override
+ public BeanItem<BEANTYPE> addItem(IDTYPE itemId, BEANTYPE bean) {
+ if (itemId != null && bean != null) {
+ return super.addItem(itemId, bean);
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Adds the bean after the given item id.
+ *
+ * @see com.vaadin.data.Container.Ordered#addItemAfter(Object, Object)
+ */
+ @Override
+ public BeanItem<BEANTYPE> addItemAfter(IDTYPE previousItemId,
+ IDTYPE newItemId, BEANTYPE bean) {
+ if (newItemId != null && bean != null) {
+ return super.addItemAfter(previousItemId, newItemId, bean);
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Adds a new bean at the given index.
+ *
+ * The bean is used both as the item contents and as the item identifier.
+ *
+ * @param index
+ * Index at which the bean should be added.
+ * @param newItemId
+ * The item id for the bean to add to the container.
+ * @param bean
+ * The bean to add to the container.
+ *
+ * @return Returns the new BeanItem or null if the operation fails.
+ */
+ @Override
+ public BeanItem<BEANTYPE> addItemAt(int index, IDTYPE newItemId,
+ BEANTYPE bean) {
+ if (newItemId != null && bean != null) {
+ return super.addItemAt(index, newItemId, bean);
+ } else {
+ return null;
+ }
+ }
+
+ // automatic item id resolution
+
+ /**
+ * Sets the bean id resolver to use a property of the beans as the
+ * identifier.
+ *
+ * @param propertyId
+ * the identifier of the property to use to find item identifiers
+ */
+ public void setBeanIdProperty(Object propertyId) {
+ setBeanIdResolver(createBeanPropertyResolver(propertyId));
+ }
+
+ @Override
+ // overridden to make public
+ public void setBeanIdResolver(
+ BeanIdResolver<IDTYPE, BEANTYPE> beanIdResolver) {
+ super.setBeanIdResolver(beanIdResolver);
+ }
+
+ @Override
+ // overridden to make public
+ public BeanItem<BEANTYPE> addBean(BEANTYPE bean)
+ throws IllegalStateException, IllegalArgumentException {
+ return super.addBean(bean);
+ }
+
+ @Override
+ // overridden to make public
+ public BeanItem<BEANTYPE> addBeanAfter(IDTYPE previousItemId, BEANTYPE bean)
+ throws IllegalStateException, IllegalArgumentException {
+ return super.addBeanAfter(previousItemId, bean);
+ }
+
+ @Override
+ // overridden to make public
+ public BeanItem<BEANTYPE> addBeanAt(int index, BEANTYPE bean)
+ throws IllegalStateException, IllegalArgumentException {
+ return super.addBeanAt(index, bean);
+ }
+
+ @Override
+ // overridden to make public
+ public void addAll(Collection<? extends BEANTYPE> collection)
+ throws IllegalStateException {
+ super.addAll(collection);
+ }
+
+}
diff --git a/server/src/com/vaadin/data/util/BeanItem.java b/server/src/com/vaadin/data/util/BeanItem.java
new file mode 100644
index 0000000000..94439471f5
--- /dev/null
+++ b/server/src/com/vaadin/data/util/BeanItem.java
@@ -0,0 +1,269 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.data.util;
+
+import java.beans.BeanInfo;
+import java.beans.IntrospectionException;
+import java.beans.Introspector;
+import java.beans.PropertyDescriptor;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * A wrapper class for adding the Item interface to any Java Bean.
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+@SuppressWarnings("serial")
+public class BeanItem<BT> extends PropertysetItem {
+
+ /**
+ * The bean which this Item is based on.
+ */
+ private final BT bean;
+
+ /**
+ * <p>
+ * Creates a new instance of <code>BeanItem</code> and adds all properties
+ * of a Java Bean to it. The properties are identified by their respective
+ * bean names.
+ * </p>
+ *
+ * <p>
+ * Note : This version only supports introspectable bean properties and
+ * their getter and setter methods. Stand-alone <code>is</code> and
+ * <code>are</code> methods are not supported.
+ * </p>
+ *
+ * @param bean
+ * the Java Bean to copy properties from.
+ *
+ */
+ public BeanItem(BT bean) {
+ this(bean, getPropertyDescriptors((Class<BT>) bean.getClass()));
+ }
+
+ /**
+ * <p>
+ * Creates a new instance of <code>BeanItem</code> using a pre-computed set
+ * of properties. The properties are identified by their respective bean
+ * names.
+ * </p>
+ *
+ * @param bean
+ * the Java Bean to copy properties from.
+ * @param propertyDescriptors
+ * pre-computed property descriptors
+ */
+ BeanItem(BT bean,
+ Map<String, VaadinPropertyDescriptor<BT>> propertyDescriptors) {
+
+ this.bean = bean;
+
+ for (VaadinPropertyDescriptor<BT> pd : propertyDescriptors.values()) {
+ addItemProperty(pd.getName(), pd.createProperty(bean));
+ }
+ }
+
+ /**
+ * <p>
+ * Creates a new instance of <code>BeanItem</code> and adds all listed
+ * properties of a Java Bean to it - in specified order. The properties are
+ * identified by their respective bean names.
+ * </p>
+ *
+ * <p>
+ * Note : This version only supports introspectable bean properties and
+ * their getter and setter methods. Stand-alone <code>is</code> and
+ * <code>are</code> methods are not supported.
+ * </p>
+ *
+ * @param bean
+ * the Java Bean to copy properties from.
+ * @param propertyIds
+ * id of the property.
+ */
+ public BeanItem(BT bean, Collection<?> propertyIds) {
+
+ this.bean = bean;
+
+ // Create bean information
+ LinkedHashMap<String, VaadinPropertyDescriptor<BT>> pds = getPropertyDescriptors((Class<BT>) bean
+ .getClass());
+
+ // Add all the bean properties as MethodProperties to this Item
+ for (Object id : propertyIds) {
+ VaadinPropertyDescriptor<BT> pd = pds.get(id);
+ if (pd != null) {
+ addItemProperty(pd.getName(), pd.createProperty(bean));
+ }
+ }
+
+ }
+
+ /**
+ * <p>
+ * Creates a new instance of <code>BeanItem</code> and adds all listed
+ * properties of a Java Bean to it - in specified order. The properties are
+ * identified by their respective bean names.
+ * </p>
+ *
+ * <p>
+ * Note : This version only supports introspectable bean properties and
+ * their getter and setter methods. Stand-alone <code>is</code> and
+ * <code>are</code> methods are not supported.
+ * </p>
+ *
+ * @param bean
+ * the Java Bean to copy properties from.
+ * @param propertyIds
+ * ids of the properties.
+ */
+ public BeanItem(BT bean, String[] propertyIds) {
+ this(bean, Arrays.asList(propertyIds));
+ }
+
+ /**
+ * <p>
+ * Perform introspection on a Java Bean class to find its properties.
+ * </p>
+ *
+ * <p>
+ * Note : This version only supports introspectable bean properties and
+ * their getter and setter methods. Stand-alone <code>is</code> and
+ * <code>are</code> methods are not supported.
+ * </p>
+ *
+ * @param beanClass
+ * the Java Bean class to get properties for.
+ * @return an ordered map from property names to property descriptors
+ */
+ static <BT> LinkedHashMap<String, VaadinPropertyDescriptor<BT>> getPropertyDescriptors(
+ final Class<BT> beanClass) {
+ final LinkedHashMap<String, VaadinPropertyDescriptor<BT>> pdMap = new LinkedHashMap<String, VaadinPropertyDescriptor<BT>>();
+
+ // Try to introspect, if it fails, we just have an empty Item
+ try {
+ List<PropertyDescriptor> propertyDescriptors = getBeanPropertyDescriptor(beanClass);
+
+ // Add all the bean properties as MethodProperties to this Item
+ // later entries on the list overwrite earlier ones
+ for (PropertyDescriptor pd : propertyDescriptors) {
+ final Method getMethod = pd.getReadMethod();
+ if ((getMethod != null)
+ && getMethod.getDeclaringClass() != Object.class) {
+ VaadinPropertyDescriptor<BT> vaadinPropertyDescriptor = new MethodPropertyDescriptor<BT>(
+ pd.getName(), pd.getPropertyType(),
+ pd.getReadMethod(), pd.getWriteMethod());
+ pdMap.put(pd.getName(), vaadinPropertyDescriptor);
+ }
+ }
+ } catch (final java.beans.IntrospectionException ignored) {
+ }
+
+ return pdMap;
+ }
+
+ /**
+ * Returns the property descriptors of a class or an interface.
+ *
+ * For an interface, superinterfaces are also iterated as Introspector does
+ * not take them into account (Oracle Java bug 4275879), but in that case,
+ * both the setter and the getter for a property must be in the same
+ * interface and should not be overridden in subinterfaces for the discovery
+ * to work correctly.
+ *
+ * For interfaces, the iteration is depth first and the properties of
+ * superinterfaces are returned before those of their subinterfaces.
+ *
+ * @param beanClass
+ * @return
+ * @throws IntrospectionException
+ */
+ private static List<PropertyDescriptor> getBeanPropertyDescriptor(
+ final Class<?> beanClass) throws IntrospectionException {
+ // Oracle bug 4275879: Introspector does not consider superinterfaces of
+ // an interface
+ if (beanClass.isInterface()) {
+ List<PropertyDescriptor> propertyDescriptors = new ArrayList<PropertyDescriptor>();
+
+ for (Class<?> cls : beanClass.getInterfaces()) {
+ propertyDescriptors.addAll(getBeanPropertyDescriptor(cls));
+ }
+
+ BeanInfo info = Introspector.getBeanInfo(beanClass);
+ propertyDescriptors.addAll(Arrays.asList(info
+ .getPropertyDescriptors()));
+
+ return propertyDescriptors;
+ } else {
+ BeanInfo info = Introspector.getBeanInfo(beanClass);
+ return Arrays.asList(info.getPropertyDescriptors());
+ }
+ }
+
+ /**
+ * Expands nested bean properties by replacing a top-level property with
+ * some or all of its sub-properties. The expansion is not recursive.
+ *
+ * @param propertyId
+ * property id for the property whose sub-properties are to be
+ * expanded,
+ * @param subPropertyIds
+ * sub-properties to expand, all sub-properties are expanded if
+ * not specified
+ */
+ public void expandProperty(String propertyId, String... subPropertyIds) {
+ Set<String> subPropertySet = new HashSet<String>(
+ Arrays.asList(subPropertyIds));
+
+ if (0 == subPropertyIds.length) {
+ // Enumerate all sub-properties
+ Class<?> propertyType = getItemProperty(propertyId).getType();
+ Map<String, ?> pds = getPropertyDescriptors(propertyType);
+ subPropertySet.addAll(pds.keySet());
+ }
+
+ for (String subproperty : subPropertySet) {
+ String qualifiedPropertyId = propertyId + "." + subproperty;
+ addNestedProperty(qualifiedPropertyId);
+ }
+
+ removeItemProperty(propertyId);
+ }
+
+ /**
+ * Adds a nested property to the item.
+ *
+ * @param nestedPropertyId
+ * property id to add. This property must not exist in the item
+ * already and must of of form "field1.field2" where field2 is a
+ * field in the object referenced to by field1
+ */
+ public void addNestedProperty(String nestedPropertyId) {
+ addItemProperty(nestedPropertyId, new NestedMethodProperty<Object>(
+ getBean(), nestedPropertyId));
+ }
+
+ /**
+ * Gets the underlying JavaBean object.
+ *
+ * @return the bean object.
+ */
+ public BT getBean() {
+ return bean;
+ }
+
+}
diff --git a/server/src/com/vaadin/data/util/BeanItemContainer.java b/server/src/com/vaadin/data/util/BeanItemContainer.java
new file mode 100644
index 0000000000..dc4deaebdc
--- /dev/null
+++ b/server/src/com/vaadin/data/util/BeanItemContainer.java
@@ -0,0 +1,241 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util;
+
+import java.util.Collection;
+
+/**
+ * An in-memory container for JavaBeans.
+ *
+ * <p>
+ * The properties of the container are determined automatically by introspecting
+ * the used JavaBean class. Only beans of the same type can be added to the
+ * container.
+ * </p>
+ *
+ * <p>
+ * BeanItemContainer uses the beans themselves as identifiers. The
+ * {@link Object#hashCode()} of a bean is used when storing and looking up beans
+ * so it must not change during the lifetime of the bean (it should not depend
+ * on any part of the bean that can be modified). Typically this restricts the
+ * implementation of {@link Object#equals(Object)} as well in order for it to
+ * fulfill the contract between {@code equals()} and {@code hashCode()}.
+ * </p>
+ *
+ * <p>
+ * To add items to the container, use the methods {@link #addBean(Object)},
+ * {@link #addBeanAfter(Object, Object)} and {@link #addBeanAt(int, Object)}.
+ * Also {@link #addItem(Object)}, {@link #addItemAfter(Object, Object)} and
+ * {@link #addItemAt(int, Object)} can be used as synonyms for them.
+ * </p>
+ *
+ * <p>
+ * It is not possible to add additional properties to the container and nested
+ * bean properties are not supported.
+ * </p>
+ *
+ * @param <BEANTYPE>
+ * The type of the Bean
+ *
+ * @since 5.4
+ */
+@SuppressWarnings("serial")
+public class BeanItemContainer<BEANTYPE> extends
+ AbstractBeanContainer<BEANTYPE, BEANTYPE> {
+
+ /**
+ * Bean identity resolver that returns the bean itself as its item
+ * identifier.
+ *
+ * This corresponds to the old behavior of {@link BeanItemContainer}, and
+ * requires suitable (identity-based) equals() and hashCode() methods on the
+ * beans.
+ *
+ * @param <BT>
+ *
+ * @since 6.5
+ */
+ private static class IdentityBeanIdResolver<BT> implements
+ BeanIdResolver<BT, BT> {
+
+ @Override
+ public BT getIdForBean(BT bean) {
+ return bean;
+ }
+
+ }
+
+ /**
+ * Constructs a {@code BeanItemContainer} for beans of the given type.
+ *
+ * @param type
+ * the type of the beans that will be added to the container.
+ * @throws IllegalArgumentException
+ * If {@code type} is null
+ */
+ public BeanItemContainer(Class<? super BEANTYPE> type)
+ throws IllegalArgumentException {
+ super(type);
+ super.setBeanIdResolver(new IdentityBeanIdResolver<BEANTYPE>());
+ }
+
+ /**
+ * Constructs a {@code BeanItemContainer} and adds the given beans to it.
+ * The collection must not be empty.
+ * {@link BeanItemContainer#BeanItemContainer(Class)} can be used for
+ * creating an initially empty {@code BeanItemContainer}.
+ *
+ * Note that when using this constructor, the actual class of the first item
+ * in the collection is used to determine the bean properties supported by
+ * the container instance, and only beans of that class or its subclasses
+ * can be added to the collection. If this is problematic or empty
+ * collections need to be supported, use {@link #BeanItemContainer(Class)}
+ * and {@link #addAll(Collection)} instead.
+ *
+ * @param collection
+ * a non empty {@link Collection} of beans.
+ * @throws IllegalArgumentException
+ * If the collection is null or empty.
+ *
+ * @deprecated use {@link #BeanItemContainer(Class, Collection)} instead
+ */
+ @SuppressWarnings("unchecked")
+ @Deprecated
+ public BeanItemContainer(Collection<? extends BEANTYPE> collection)
+ throws IllegalArgumentException {
+ // must assume the class is BT
+ // the class information is erased by the compiler
+ this((Class<BEANTYPE>) getBeanClassForCollection(collection),
+ collection);
+ }
+
+ /**
+ * Internal helper method to support the deprecated {@link Collection}
+ * container.
+ *
+ * @param <BT>
+ * @param collection
+ * @return
+ * @throws IllegalArgumentException
+ */
+ @SuppressWarnings("unchecked")
+ @Deprecated
+ private static <BT> Class<? extends BT> getBeanClassForCollection(
+ Collection<? extends BT> collection)
+ throws IllegalArgumentException {
+ if (collection == null || collection.isEmpty()) {
+ throw new IllegalArgumentException(
+ "The collection passed to BeanItemContainer constructor must not be null or empty. Use the other BeanItemContainer constructor.");
+ }
+ return (Class<? extends BT>) collection.iterator().next().getClass();
+ }
+
+ /**
+ * Constructs a {@code BeanItemContainer} and adds the given beans to it.
+ *
+ * @param type
+ * the type of the beans that will be added to the container.
+ * @param collection
+ * a {@link Collection} of beans (can be empty or null).
+ * @throws IllegalArgumentException
+ * If {@code type} is null
+ */
+ public BeanItemContainer(Class<? super BEANTYPE> type,
+ Collection<? extends BEANTYPE> collection)
+ throws IllegalArgumentException {
+ super(type);
+ super.setBeanIdResolver(new IdentityBeanIdResolver<BEANTYPE>());
+
+ if (collection != null) {
+ addAll(collection);
+ }
+ }
+
+ /**
+ * Adds all the beans from a {@link Collection} in one go. More efficient
+ * than adding them one by one.
+ *
+ * @param collection
+ * The collection of beans to add. Must not be null.
+ */
+ @Override
+ public void addAll(Collection<? extends BEANTYPE> collection) {
+ super.addAll(collection);
+ }
+
+ /**
+ * Adds the bean after the given bean.
+ *
+ * The bean is used both as the item contents and as the item identifier.
+ *
+ * @param previousItemId
+ * the bean (of type BT) after which to add newItemId
+ * @param newItemId
+ * the bean (of type BT) to add (not null)
+ *
+ * @see com.vaadin.data.Container.Ordered#addItemAfter(Object, Object)
+ */
+ @Override
+ @SuppressWarnings("unchecked")
+ public BeanItem<BEANTYPE> addItemAfter(Object previousItemId,
+ Object newItemId) throws IllegalArgumentException {
+ return super.addBeanAfter((BEANTYPE) previousItemId,
+ (BEANTYPE) newItemId);
+ }
+
+ /**
+ * Adds a new bean at the given index.
+ *
+ * The bean is used both as the item contents and as the item identifier.
+ *
+ * @param index
+ * Index at which the bean should be added.
+ * @param newItemId
+ * The bean to add to the container.
+ * @return Returns the new BeanItem or null if the operation fails.
+ */
+ @Override
+ @SuppressWarnings("unchecked")
+ public BeanItem<BEANTYPE> addItemAt(int index, Object newItemId)
+ throws IllegalArgumentException {
+ return super.addBeanAt(index, (BEANTYPE) newItemId);
+ }
+
+ /**
+ * Adds the bean to the Container.
+ *
+ * The bean is used both as the item contents and as the item identifier.
+ *
+ * @see com.vaadin.data.Container#addItem(Object)
+ */
+ @Override
+ @SuppressWarnings("unchecked")
+ public BeanItem<BEANTYPE> addItem(Object itemId) {
+ return super.addBean((BEANTYPE) itemId);
+ }
+
+ /**
+ * Adds the bean to the Container.
+ *
+ * The bean is used both as the item contents and as the item identifier.
+ *
+ * @see com.vaadin.data.Container#addItem(Object)
+ */
+ @Override
+ public BeanItem<BEANTYPE> addBean(BEANTYPE bean) {
+ return addItem(bean);
+ }
+
+ /**
+ * Unsupported in BeanItemContainer.
+ */
+ @Override
+ protected void setBeanIdResolver(
+ AbstractBeanContainer.BeanIdResolver<BEANTYPE, BEANTYPE> beanIdResolver)
+ throws UnsupportedOperationException {
+ throw new UnsupportedOperationException(
+ "BeanItemContainer always uses an IdentityBeanIdResolver");
+ }
+
+}
diff --git a/server/src/com/vaadin/data/util/ContainerHierarchicalWrapper.java b/server/src/com/vaadin/data/util/ContainerHierarchicalWrapper.java
new file mode 100644
index 0000000000..717ce834cf
--- /dev/null
+++ b/server/src/com/vaadin/data/util/ContainerHierarchicalWrapper.java
@@ -0,0 +1,792 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.data.util;
+
+import java.io.Serializable;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.Hashtable;
+import java.util.Iterator;
+import java.util.LinkedHashSet;
+import java.util.LinkedList;
+
+import com.vaadin.data.Container;
+import com.vaadin.data.Item;
+import com.vaadin.data.Property;
+
+/**
+ * <p>
+ * A wrapper class for adding external hierarchy to containers not implementing
+ * the {@link com.vaadin.data.Container.Hierarchical} interface.
+ * </p>
+ *
+ * <p>
+ * If the wrapped container is changed directly (that is, not through the
+ * wrapper), and does not implement Container.ItemSetChangeNotifier and/or
+ * Container.PropertySetChangeNotifier the hierarchy information must be updated
+ * with the {@link #updateHierarchicalWrapper()} method.
+ * </p>
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+@SuppressWarnings("serial")
+public class ContainerHierarchicalWrapper implements Container.Hierarchical,
+ Container.ItemSetChangeNotifier, Container.PropertySetChangeNotifier {
+
+ /** The wrapped container */
+ private final Container container;
+
+ /** Set of IDs of those contained Items that can't have children. */
+ private HashSet<Object> noChildrenAllowed = null;
+
+ /** Mapping from Item ID to parent Item ID */
+ private Hashtable<Object, Object> parent = null;
+
+ /** Mapping from Item ID to a list of child IDs */
+ private Hashtable<Object, LinkedList<Object>> children = null;
+
+ /** List that contains all root elements of the container. */
+ private LinkedHashSet<Object> roots = null;
+
+ /** Is the wrapped container hierarchical by itself ? */
+ private boolean hierarchical;
+
+ /**
+ * A comparator that sorts the listed items before other items. Otherwise,
+ * the order is undefined.
+ */
+ private static class ListedItemsFirstComparator implements
+ Comparator<Object>, Serializable {
+ private final Collection<?> itemIds;
+
+ private ListedItemsFirstComparator(Collection<?> itemIds) {
+ this.itemIds = itemIds;
+ }
+
+ @Override
+ public int compare(Object o1, Object o2) {
+ if (o1.equals(o2)) {
+ return 0;
+ }
+ for (Object id : itemIds) {
+ if (id == o1) {
+ return -1;
+ } else if (id == o2) {
+ return 1;
+ }
+ }
+ return 0;
+ }
+ };
+
+ /**
+ * Constructs a new hierarchical wrapper for an existing Container. Works
+ * even if the to-be-wrapped container already implements the
+ * <code>Container.Hierarchical</code> interface.
+ *
+ * @param toBeWrapped
+ * the container that needs to be accessed hierarchically
+ * @see #updateHierarchicalWrapper()
+ */
+ public ContainerHierarchicalWrapper(Container toBeWrapped) {
+
+ container = toBeWrapped;
+ hierarchical = container instanceof Container.Hierarchical;
+
+ // Check arguments
+ if (container == null) {
+ throw new NullPointerException("Null can not be wrapped");
+ }
+
+ // Create initial order if needed
+ if (!hierarchical) {
+ noChildrenAllowed = new HashSet<Object>();
+ parent = new Hashtable<Object, Object>();
+ children = new Hashtable<Object, LinkedList<Object>>();
+ roots = new LinkedHashSet<Object>(container.getItemIds());
+ }
+
+ updateHierarchicalWrapper();
+
+ }
+
+ /**
+ * Updates the wrapper's internal hierarchy data to include all Items in the
+ * underlying container. If the contents of the wrapped container change
+ * without the wrapper's knowledge, this method needs to be called to update
+ * the hierarchy information of the Items.
+ */
+ public void updateHierarchicalWrapper() {
+
+ if (!hierarchical) {
+
+ // Recreate hierarchy and data structures if missing
+ if (noChildrenAllowed == null || parent == null || children == null
+ || roots == null) {
+ noChildrenAllowed = new HashSet<Object>();
+ parent = new Hashtable<Object, Object>();
+ children = new Hashtable<Object, LinkedList<Object>>();
+ roots = new LinkedHashSet<Object>(container.getItemIds());
+ }
+
+ // Check that the hierarchy is up-to-date
+ else {
+
+ // ensure order of root and child lists is same as in wrapped
+ // container
+ Collection<?> itemIds = container.getItemIds();
+ Comparator<Object> basedOnOrderFromWrappedContainer = new ListedItemsFirstComparator(
+ itemIds);
+
+ // Calculate the set of all items in the hierarchy
+ final HashSet<Object> s = new HashSet<Object>();
+ s.addAll(parent.keySet());
+ s.addAll(children.keySet());
+ s.addAll(roots);
+
+ // Remove unnecessary items
+ for (final Iterator<Object> i = s.iterator(); i.hasNext();) {
+ final Object id = i.next();
+ if (!container.containsId(id)) {
+ removeFromHierarchyWrapper(id);
+ }
+ }
+
+ // Add all the missing items
+ final Collection<?> ids = container.getItemIds();
+ for (final Iterator<?> i = ids.iterator(); i.hasNext();) {
+ final Object id = i.next();
+ if (!s.contains(id)) {
+ addToHierarchyWrapper(id);
+ s.add(id);
+ }
+ }
+
+ Object[] array = roots.toArray();
+ Arrays.sort(array, basedOnOrderFromWrappedContainer);
+ roots = new LinkedHashSet<Object>();
+ for (int i = 0; i < array.length; i++) {
+ roots.add(array[i]);
+ }
+ for (Object object : children.keySet()) {
+ LinkedList<Object> object2 = children.get(object);
+ Collections.sort(object2, basedOnOrderFromWrappedContainer);
+ }
+
+ }
+ }
+ }
+
+ /**
+ * Removes the specified Item from the wrapper's internal hierarchy
+ * structure.
+ * <p>
+ * Note : The Item is not removed from the underlying Container.
+ * </p>
+ *
+ * @param itemId
+ * the ID of the item to remove from the hierarchy.
+ */
+ private void removeFromHierarchyWrapper(Object itemId) {
+
+ LinkedList<Object> oprhanedChildren = children.remove(itemId);
+ if (oprhanedChildren != null) {
+ for (Object object : oprhanedChildren) {
+ // make orphaned children root nodes
+ setParent(object, null);
+ }
+ }
+
+ roots.remove(itemId);
+ final Object p = parent.get(itemId);
+ if (p != null) {
+ final LinkedList<Object> c = children.get(p);
+ if (c != null) {
+ c.remove(itemId);
+ }
+ }
+ parent.remove(itemId);
+ noChildrenAllowed.remove(itemId);
+ }
+
+ /**
+ * Adds the specified Item specified to the internal hierarchy structure.
+ * The new item is added as a root Item. The underlying container is not
+ * modified.
+ *
+ * @param itemId
+ * the ID of the item to add to the hierarchy.
+ */
+ private void addToHierarchyWrapper(Object itemId) {
+ roots.add(itemId);
+
+ }
+
+ /*
+ * Can the specified Item have any children? Don't add a JavaDoc comment
+ * here, we use the default documentation from implemented interface.
+ */
+ @Override
+ public boolean areChildrenAllowed(Object itemId) {
+
+ // If the wrapped container implements the method directly, use it
+ if (hierarchical) {
+ return ((Container.Hierarchical) container)
+ .areChildrenAllowed(itemId);
+ }
+
+ if (noChildrenAllowed.contains(itemId)) {
+ return false;
+ }
+
+ return containsId(itemId);
+ }
+
+ /*
+ * Gets the IDs of the children of the specified Item. Don't add a JavaDoc
+ * comment here, we use the default documentation from implemented
+ * interface.
+ */
+ @Override
+ public Collection<?> getChildren(Object itemId) {
+
+ // If the wrapped container implements the method directly, use it
+ if (hierarchical) {
+ return ((Container.Hierarchical) container).getChildren(itemId);
+ }
+
+ final Collection<?> c = children.get(itemId);
+ if (c == null) {
+ return null;
+ }
+ return Collections.unmodifiableCollection(c);
+ }
+
+ /*
+ * Gets the ID of the parent of the specified Item. Don't add a JavaDoc
+ * comment here, we use the default documentation from implemented
+ * interface.
+ */
+ @Override
+ public Object getParent(Object itemId) {
+
+ // If the wrapped container implements the method directly, use it
+ if (hierarchical) {
+ return ((Container.Hierarchical) container).getParent(itemId);
+ }
+
+ return parent.get(itemId);
+ }
+
+ /*
+ * Is the Item corresponding to the given ID a leaf node? Don't add a
+ * JavaDoc comment here, we use the default documentation from implemented
+ * interface.
+ */
+ @Override
+ public boolean hasChildren(Object itemId) {
+
+ // If the wrapped container implements the method directly, use it
+ if (hierarchical) {
+ return ((Container.Hierarchical) container).hasChildren(itemId);
+ }
+
+ LinkedList<Object> list = children.get(itemId);
+ return (list != null && !list.isEmpty());
+ }
+
+ /*
+ * Is the Item corresponding to the given ID a root node? Don't add a
+ * JavaDoc comment here, we use the default documentation from implemented
+ * interface.
+ */
+ @Override
+ public boolean isRoot(Object itemId) {
+
+ // If the wrapped container implements the method directly, use it
+ if (hierarchical) {
+ return ((Container.Hierarchical) container).isRoot(itemId);
+ }
+
+ if (parent.containsKey(itemId)) {
+ return false;
+ }
+
+ return containsId(itemId);
+ }
+
+ /*
+ * Gets the IDs of the root elements in the container. Don't add a JavaDoc
+ * comment here, we use the default documentation from implemented
+ * interface.
+ */
+ @Override
+ public Collection<?> rootItemIds() {
+
+ // If the wrapped container implements the method directly, use it
+ if (hierarchical) {
+ return ((Container.Hierarchical) container).rootItemIds();
+ }
+
+ return Collections.unmodifiableCollection(roots);
+ }
+
+ /**
+ * <p>
+ * Sets the given Item's capability to have children. If the Item identified
+ * with the itemId already has children and the areChildrenAllowed is false
+ * this method fails and <code>false</code> is returned; the children must
+ * be first explicitly removed with
+ * {@link #setParent(Object itemId, Object newParentId)} or
+ * {@link com.vaadin.data.Container#removeItem(Object itemId)}.
+ * </p>
+ *
+ * @param itemId
+ * the ID of the Item in the container whose child capability is
+ * to be set.
+ * @param childrenAllowed
+ * the boolean value specifying if the Item can have children or
+ * not.
+ * @return <code>true</code> if the operation succeeded, <code>false</code>
+ * if not
+ */
+ @Override
+ public boolean setChildrenAllowed(Object itemId, boolean childrenAllowed) {
+
+ // If the wrapped container implements the method directly, use it
+ if (hierarchical) {
+ return ((Container.Hierarchical) container).setChildrenAllowed(
+ itemId, childrenAllowed);
+ }
+
+ // Check that the item is in the container
+ if (!containsId(itemId)) {
+ return false;
+ }
+
+ // Update status
+ if (childrenAllowed) {
+ noChildrenAllowed.remove(itemId);
+ } else {
+ noChildrenAllowed.add(itemId);
+ }
+
+ return true;
+ }
+
+ /**
+ * <p>
+ * Sets the parent of an Item. The new parent item must exist and be able to
+ * have children. (<code>canHaveChildren(newParentId) == true</code>). It is
+ * also possible to detach a node from the hierarchy (and thus make it root)
+ * by setting the parent <code>null</code>.
+ * </p>
+ *
+ * @param itemId
+ * the ID of the item to be set as the child of the Item
+ * identified with newParentId.
+ * @param newParentId
+ * the ID of the Item that's to be the new parent of the Item
+ * identified with itemId.
+ * @return <code>true</code> if the operation succeeded, <code>false</code>
+ * if not
+ */
+ @Override
+ public boolean setParent(Object itemId, Object newParentId) {
+
+ // If the wrapped container implements the method directly, use it
+ if (hierarchical) {
+ return ((Container.Hierarchical) container).setParent(itemId,
+ newParentId);
+ }
+
+ // Check that the item is in the container
+ if (!containsId(itemId)) {
+ return false;
+ }
+
+ // Get the old parent
+ final Object oldParentId = parent.get(itemId);
+
+ // Check if no change is necessary
+ if ((newParentId == null && oldParentId == null)
+ || (newParentId != null && newParentId.equals(oldParentId))) {
+ return true;
+ }
+
+ // Making root
+ if (newParentId == null) {
+
+ // Remove from old parents children list
+ final LinkedList<Object> l = children.get(oldParentId);
+ if (l != null) {
+ l.remove(itemId);
+ if (l.isEmpty()) {
+ children.remove(itemId);
+ }
+ }
+
+ // Add to be a root
+ roots.add(itemId);
+
+ // Update parent
+ parent.remove(itemId);
+
+ return true;
+ }
+
+ // Check that the new parent exists in container and can have
+ // children
+ if (!containsId(newParentId) || noChildrenAllowed.contains(newParentId)) {
+ return false;
+ }
+
+ // Check that setting parent doesn't result to a loop
+ Object o = newParentId;
+ while (o != null && !o.equals(itemId)) {
+ o = parent.get(o);
+ }
+ if (o != null) {
+ return false;
+ }
+
+ // Update parent
+ parent.put(itemId, newParentId);
+ LinkedList<Object> pcl = children.get(newParentId);
+ if (pcl == null) {
+ pcl = new LinkedList<Object>();
+ children.put(newParentId, pcl);
+ }
+ pcl.add(itemId);
+
+ // Remove from old parent or root
+ if (oldParentId == null) {
+ roots.remove(itemId);
+ } else {
+ final LinkedList<Object> l = children.get(oldParentId);
+ if (l != null) {
+ l.remove(itemId);
+ if (l.isEmpty()) {
+ children.remove(oldParentId);
+ }
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Creates a new Item into the Container, assigns it an automatic ID, and
+ * adds it to the hierarchy.
+ *
+ * @return the autogenerated ID of the new Item or <code>null</code> if the
+ * operation failed
+ * @throws UnsupportedOperationException
+ * if the addItem is not supported.
+ */
+ @Override
+ public Object addItem() throws UnsupportedOperationException {
+
+ final Object id = container.addItem();
+ if (!hierarchical && id != null) {
+ addToHierarchyWrapper(id);
+ }
+ return id;
+ }
+
+ /**
+ * Adds a new Item by its ID to the underlying container and to the
+ * hierarchy.
+ *
+ * @param itemId
+ * the ID of the Item to be created.
+ * @return the added Item or <code>null</code> if the operation failed.
+ * @throws UnsupportedOperationException
+ * if the addItem is not supported.
+ */
+ @Override
+ public Item addItem(Object itemId) throws UnsupportedOperationException {
+
+ // Null ids are not accepted
+ if (itemId == null) {
+ throw new NullPointerException("Container item id can not be null");
+ }
+
+ final Item item = container.addItem(itemId);
+ if (!hierarchical && item != null) {
+ addToHierarchyWrapper(itemId);
+ }
+ return item;
+ }
+
+ /**
+ * Removes all items from the underlying container and from the hierarcy.
+ *
+ * @return <code>true</code> if the operation succeeded, <code>false</code>
+ * if not
+ * @throws UnsupportedOperationException
+ * if the removeAllItems is not supported.
+ */
+ @Override
+ public boolean removeAllItems() throws UnsupportedOperationException {
+
+ final boolean success = container.removeAllItems();
+
+ if (!hierarchical && success) {
+ roots.clear();
+ parent.clear();
+ children.clear();
+ noChildrenAllowed.clear();
+ }
+ return success;
+ }
+
+ /**
+ * Removes an Item specified by the itemId from the underlying container and
+ * from the hierarchy.
+ *
+ * @param itemId
+ * the ID of the Item to be removed.
+ * @return <code>true</code> if the operation succeeded, <code>false</code>
+ * if not
+ * @throws UnsupportedOperationException
+ * if the removeItem is not supported.
+ */
+ @Override
+ public boolean removeItem(Object itemId)
+ throws UnsupportedOperationException {
+
+ final boolean success = container.removeItem(itemId);
+
+ if (!hierarchical && success) {
+ removeFromHierarchyWrapper(itemId);
+ }
+
+ return success;
+ }
+
+ /**
+ * Removes the Item identified by given itemId and all its children.
+ *
+ * @see #removeItem(Object)
+ * @param itemId
+ * the identifier of the Item to be removed
+ * @return true if the operation succeeded
+ */
+ public boolean removeItemRecursively(Object itemId) {
+ return HierarchicalContainer.removeItemRecursively(this, itemId);
+ }
+
+ /**
+ * Adds a new Property to all Items in the Container.
+ *
+ * @param propertyId
+ * the ID of the new Property.
+ * @param type
+ * the Data type of the new Property.
+ * @param defaultValue
+ * the value all created Properties are initialized to.
+ * @return <code>true</code> if the operation succeeded, <code>false</code>
+ * if not
+ * @throws UnsupportedOperationException
+ * if the addContainerProperty is not supported.
+ */
+ @Override
+ public boolean addContainerProperty(Object propertyId, Class<?> type,
+ Object defaultValue) throws UnsupportedOperationException {
+
+ return container.addContainerProperty(propertyId, type, defaultValue);
+ }
+
+ /**
+ * Removes the specified Property from the underlying container and from the
+ * hierarchy.
+ * <p>
+ * Note : The Property will be removed from all Items in the Container.
+ * </p>
+ *
+ * @param propertyId
+ * the ID of the Property to remove.
+ * @return <code>true</code> if the operation succeeded, <code>false</code>
+ * if not
+ * @throws UnsupportedOperationException
+ * if the removeContainerProperty is not supported.
+ */
+ @Override
+ public boolean removeContainerProperty(Object propertyId)
+ throws UnsupportedOperationException {
+ return container.removeContainerProperty(propertyId);
+ }
+
+ /*
+ * Does the container contain the specified Item? Don't add a JavaDoc
+ * comment here, we use the default documentation from implemented
+ * interface.
+ */
+ @Override
+ public boolean containsId(Object itemId) {
+ return container.containsId(itemId);
+ }
+
+ /*
+ * Gets the specified Item from the container. Don't add a JavaDoc comment
+ * here, we use the default documentation from implemented interface.
+ */
+ @Override
+ public Item getItem(Object itemId) {
+ return container.getItem(itemId);
+ }
+
+ /*
+ * Gets the ID's of all Items stored in the Container Don't add a JavaDoc
+ * comment here, we use the default documentation from implemented
+ * interface.
+ */
+ @Override
+ public Collection<?> getItemIds() {
+ return container.getItemIds();
+ }
+
+ /*
+ * Gets the Property identified by the given itemId and propertyId from the
+ * Container Don't add a JavaDoc comment here, we use the default
+ * documentation from implemented interface.
+ */
+ @Override
+ public Property<?> getContainerProperty(Object itemId, Object propertyId) {
+ return container.getContainerProperty(itemId, propertyId);
+ }
+
+ /*
+ * Gets the ID's of all Properties stored in the Container Don't add a
+ * JavaDoc comment here, we use the default documentation from implemented
+ * interface.
+ */
+ @Override
+ public Collection<?> getContainerPropertyIds() {
+ return container.getContainerPropertyIds();
+ }
+
+ /*
+ * Gets the data type of all Properties identified by the given Property ID.
+ * Don't add a JavaDoc comment here, we use the default documentation from
+ * implemented interface.
+ */
+ @Override
+ public Class<?> getType(Object propertyId) {
+ return container.getType(propertyId);
+ }
+
+ /*
+ * Gets the number of Items in the Container. Don't add a JavaDoc comment
+ * here, we use the default documentation from implemented interface.
+ */
+ @Override
+ public int size() {
+ return container.size();
+ }
+
+ /*
+ * Registers a new Item set change listener for this Container. Don't add a
+ * JavaDoc comment here, we use the default documentation from implemented
+ * interface.
+ */
+ @Override
+ public void addListener(Container.ItemSetChangeListener listener) {
+ if (container instanceof Container.ItemSetChangeNotifier) {
+ ((Container.ItemSetChangeNotifier) container)
+ .addListener(new PiggybackListener(listener));
+ }
+ }
+
+ /*
+ * Removes a Item set change listener from the object. Don't add a JavaDoc
+ * comment here, we use the default documentation from implemented
+ * interface.
+ */
+ @Override
+ public void removeListener(Container.ItemSetChangeListener listener) {
+ if (container instanceof Container.ItemSetChangeNotifier) {
+ ((Container.ItemSetChangeNotifier) container)
+ .removeListener(new PiggybackListener(listener));
+ }
+ }
+
+ /*
+ * Registers a new Property set change listener for this Container. Don't
+ * add a JavaDoc comment here, we use the default documentation from
+ * implemented interface.
+ */
+ @Override
+ public void addListener(Container.PropertySetChangeListener listener) {
+ if (container instanceof Container.PropertySetChangeNotifier) {
+ ((Container.PropertySetChangeNotifier) container)
+ .addListener(new PiggybackListener(listener));
+ }
+ }
+
+ /*
+ * Removes a Property set change listener from the object. Don't add a
+ * JavaDoc comment here, we use the default documentation from implemented
+ * interface.
+ */
+ @Override
+ public void removeListener(Container.PropertySetChangeListener listener) {
+ if (container instanceof Container.PropertySetChangeNotifier) {
+ ((Container.PropertySetChangeNotifier) container)
+ .removeListener(new PiggybackListener(listener));
+ }
+ }
+
+ /**
+ * This listener 'piggybacks' on the real listener in order to update the
+ * wrapper when needed. It proxies equals() and hashCode() to the real
+ * listener so that the correct listener gets removed.
+ *
+ */
+ private class PiggybackListener implements
+ Container.PropertySetChangeListener,
+ Container.ItemSetChangeListener {
+
+ Object listener;
+
+ public PiggybackListener(Object realListener) {
+ listener = realListener;
+ }
+
+ @Override
+ public void containerItemSetChange(ItemSetChangeEvent event) {
+ updateHierarchicalWrapper();
+ ((Container.ItemSetChangeListener) listener)
+ .containerItemSetChange(event);
+
+ }
+
+ @Override
+ public void containerPropertySetChange(PropertySetChangeEvent event) {
+ updateHierarchicalWrapper();
+ ((Container.PropertySetChangeListener) listener)
+ .containerPropertySetChange(event);
+
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ return obj == listener || (obj != null && obj.equals(listener));
+ }
+
+ @Override
+ public int hashCode() {
+ return listener.hashCode();
+ }
+
+ }
+}
diff --git a/server/src/com/vaadin/data/util/ContainerOrderedWrapper.java b/server/src/com/vaadin/data/util/ContainerOrderedWrapper.java
new file mode 100644
index 0000000000..d3d6f88d3e
--- /dev/null
+++ b/server/src/com/vaadin/data/util/ContainerOrderedWrapper.java
@@ -0,0 +1,644 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.data.util;
+
+import java.util.Collection;
+import java.util.Hashtable;
+import java.util.Iterator;
+import java.util.LinkedList;
+
+import com.vaadin.data.Container;
+import com.vaadin.data.Item;
+import com.vaadin.data.Property;
+
+/**
+ * <p>
+ * A wrapper class for adding external ordering to containers not implementing
+ * the {@link com.vaadin.data.Container.Ordered} interface.
+ * </p>
+ *
+ * <p>
+ * If the wrapped container is changed directly (that is, not through the
+ * wrapper), and does not implement Container.ItemSetChangeNotifier and/or
+ * Container.PropertySetChangeNotifier the hierarchy information must be updated
+ * with the {@link #updateOrderWrapper()} method.
+ * </p>
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+@SuppressWarnings("serial")
+public class ContainerOrderedWrapper implements Container.Ordered,
+ Container.ItemSetChangeNotifier, Container.PropertySetChangeNotifier {
+
+ /**
+ * The wrapped container
+ */
+ private final Container container;
+
+ /**
+ * Ordering information, ie. the mapping from Item ID to the next item ID
+ */
+ private Hashtable<Object, Object> next;
+
+ /**
+ * Reverse ordering information for convenience and performance reasons.
+ */
+ private Hashtable<Object, Object> prev;
+
+ /**
+ * ID of the first Item in the container.
+ */
+ private Object first;
+
+ /**
+ * ID of the last Item in the container.
+ */
+ private Object last;
+
+ /**
+ * Is the wrapped container ordered by itself, ie. does it implement the
+ * Container.Ordered interface by itself? If it does, this class will use
+ * the methods of the underlying container directly.
+ */
+ private boolean ordered = false;
+
+ /**
+ * The last known size of the wrapped container. Used to check whether items
+ * have been added or removed to the wrapped container, when the wrapped
+ * container does not send ItemSetChangeEvents.
+ */
+ private int lastKnownSize = -1;
+
+ /**
+ * Constructs a new ordered wrapper for an existing Container. Works even if
+ * the to-be-wrapped container already implements the Container.Ordered
+ * interface.
+ *
+ * @param toBeWrapped
+ * the container whose contents need to be ordered.
+ */
+ public ContainerOrderedWrapper(Container toBeWrapped) {
+
+ container = toBeWrapped;
+ ordered = container instanceof Container.Ordered;
+
+ // Checks arguments
+ if (container == null) {
+ throw new NullPointerException("Null can not be wrapped");
+ }
+
+ // Creates initial order if needed
+ updateOrderWrapper();
+ }
+
+ /**
+ * Removes the specified Item from the wrapper's internal hierarchy
+ * structure.
+ * <p>
+ * Note : The Item is not removed from the underlying Container.
+ * </p>
+ *
+ * @param id
+ * the ID of the Item to be removed from the ordering.
+ */
+ private void removeFromOrderWrapper(Object id) {
+ if (id != null) {
+ final Object pid = prev.get(id);
+ final Object nid = next.get(id);
+ if (first.equals(id)) {
+ first = nid;
+ }
+ if (last.equals(id)) {
+ first = pid;
+ }
+ if (nid != null) {
+ prev.put(nid, pid);
+ }
+ if (pid != null) {
+ next.put(pid, nid);
+ }
+ next.remove(id);
+ prev.remove(id);
+ }
+ }
+
+ /**
+ * Registers the specified Item to the last position in the wrapper's
+ * internal ordering. The underlying container is not modified.
+ *
+ * @param id
+ * the ID of the Item to be added to the ordering.
+ */
+ private void addToOrderWrapper(Object id) {
+
+ // Adds the if to tail
+ if (last != null) {
+ next.put(last, id);
+ prev.put(id, last);
+ last = id;
+ } else {
+ first = last = id;
+ }
+ }
+
+ /**
+ * Registers the specified Item after the specified itemId in the wrapper's
+ * internal ordering. The underlying container is not modified. Given item
+ * id must be in the container, or must be null.
+ *
+ * @param id
+ * the ID of the Item to be added to the ordering.
+ * @param previousItemId
+ * the Id of the previous item.
+ */
+ private void addToOrderWrapper(Object id, Object previousItemId) {
+
+ if (last == previousItemId || last == null) {
+ addToOrderWrapper(id);
+ } else {
+ if (previousItemId == null) {
+ next.put(id, first);
+ prev.put(first, id);
+ first = id;
+ } else {
+ prev.put(id, previousItemId);
+ next.put(id, next.get(previousItemId));
+ prev.put(next.get(previousItemId), id);
+ next.put(previousItemId, id);
+ }
+ }
+ }
+
+ /**
+ * Updates the wrapper's internal ordering information to include all Items
+ * in the underlying container.
+ * <p>
+ * Note : If the contents of the wrapped container change without the
+ * wrapper's knowledge, this method needs to be called to update the
+ * ordering information of the Items.
+ * </p>
+ */
+ public void updateOrderWrapper() {
+
+ if (!ordered) {
+
+ final Collection<?> ids = container.getItemIds();
+
+ // Recreates ordering if some parts of it are missing
+ if (next == null || first == null || last == null || prev != null) {
+ first = null;
+ last = null;
+ next = new Hashtable<Object, Object>();
+ prev = new Hashtable<Object, Object>();
+ }
+
+ // Filter out all the missing items
+ final LinkedList<?> l = new LinkedList<Object>(next.keySet());
+ for (final Iterator<?> i = l.iterator(); i.hasNext();) {
+ final Object id = i.next();
+ if (!container.containsId(id)) {
+ removeFromOrderWrapper(id);
+ }
+ }
+
+ // Adds missing items
+ for (final Iterator<?> i = ids.iterator(); i.hasNext();) {
+ final Object id = i.next();
+ if (!next.containsKey(id)) {
+ addToOrderWrapper(id);
+ }
+ }
+ }
+ }
+
+ /*
+ * Gets the first item stored in the ordered container Don't add a JavaDoc
+ * comment here, we use the default documentation from implemented
+ * interface.
+ */
+ @Override
+ public Object firstItemId() {
+ if (ordered) {
+ return ((Container.Ordered) container).firstItemId();
+ }
+ return first;
+ }
+
+ /*
+ * Tests if the given item is the first item in the container Don't add a
+ * JavaDoc comment here, we use the default documentation from implemented
+ * interface.
+ */
+ @Override
+ public boolean isFirstId(Object itemId) {
+ if (ordered) {
+ return ((Container.Ordered) container).isFirstId(itemId);
+ }
+ return first != null && first.equals(itemId);
+ }
+
+ /*
+ * Tests if the given item is the last item in the container Don't add a
+ * JavaDoc comment here, we use the default documentation from implemented
+ * interface.
+ */
+ @Override
+ public boolean isLastId(Object itemId) {
+ if (ordered) {
+ return ((Container.Ordered) container).isLastId(itemId);
+ }
+ return last != null && last.equals(itemId);
+ }
+
+ /*
+ * Gets the last item stored in the ordered container Don't add a JavaDoc
+ * comment here, we use the default documentation from implemented
+ * interface.
+ */
+ @Override
+ public Object lastItemId() {
+ if (ordered) {
+ return ((Container.Ordered) container).lastItemId();
+ }
+ return last;
+ }
+
+ /*
+ * Gets the item that is next from the specified item. Don't add a JavaDoc
+ * comment here, we use the default documentation from implemented
+ * interface.
+ */
+ @Override
+ public Object nextItemId(Object itemId) {
+ if (ordered) {
+ return ((Container.Ordered) container).nextItemId(itemId);
+ }
+ if (itemId == null) {
+ return null;
+ }
+ return next.get(itemId);
+ }
+
+ /*
+ * Gets the item that is previous from the specified item. Don't add a
+ * JavaDoc comment here, we use the default documentation from implemented
+ * interface.
+ */
+ @Override
+ public Object prevItemId(Object itemId) {
+ if (ordered) {
+ return ((Container.Ordered) container).prevItemId(itemId);
+ }
+ if (itemId == null) {
+ return null;
+ }
+ return prev.get(itemId);
+ }
+
+ /**
+ * Registers a new Property to all Items in the Container.
+ *
+ * @param propertyId
+ * the ID of the new Property.
+ * @param type
+ * the Data type of the new Property.
+ * @param defaultValue
+ * the value all created Properties are initialized to.
+ * @return <code>true</code> if the operation succeeded, <code>false</code>
+ * if not
+ */
+ @Override
+ public boolean addContainerProperty(Object propertyId, Class<?> type,
+ Object defaultValue) throws UnsupportedOperationException {
+
+ return container.addContainerProperty(propertyId, type, defaultValue);
+ }
+
+ /**
+ * Creates a new Item into the Container, assigns it an automatic ID, and
+ * adds it to the ordering.
+ *
+ * @return the autogenerated ID of the new Item or <code>null</code> if the
+ * operation failed
+ * @throws UnsupportedOperationException
+ * if the addItem is not supported.
+ */
+ @Override
+ public Object addItem() throws UnsupportedOperationException {
+
+ final Object id = container.addItem();
+ if (!ordered && id != null) {
+ addToOrderWrapper(id);
+ }
+ return id;
+ }
+
+ /**
+ * Registers a new Item by its ID to the underlying container and to the
+ * ordering.
+ *
+ * @param itemId
+ * the ID of the Item to be created.
+ * @return the added Item or <code>null</code> if the operation failed
+ * @throws UnsupportedOperationException
+ * if the addItem is not supported.
+ */
+ @Override
+ public Item addItem(Object itemId) throws UnsupportedOperationException {
+ final Item item = container.addItem(itemId);
+ if (!ordered && item != null) {
+ addToOrderWrapper(itemId);
+ }
+ return item;
+ }
+
+ /**
+ * Removes all items from the underlying container and from the ordering.
+ *
+ * @return <code>true</code> if the operation succeeded, otherwise
+ * <code>false</code>
+ * @throws UnsupportedOperationException
+ * if the removeAllItems is not supported.
+ */
+ @Override
+ public boolean removeAllItems() throws UnsupportedOperationException {
+ final boolean success = container.removeAllItems();
+ if (!ordered && success) {
+ first = last = null;
+ next.clear();
+ prev.clear();
+ }
+ return success;
+ }
+
+ /**
+ * Removes an Item specified by the itemId from the underlying container and
+ * from the ordering.
+ *
+ * @param itemId
+ * the ID of the Item to be removed.
+ * @return <code>true</code> if the operation succeeded, <code>false</code>
+ * if not
+ * @throws UnsupportedOperationException
+ * if the removeItem is not supported.
+ */
+ @Override
+ public boolean removeItem(Object itemId)
+ throws UnsupportedOperationException {
+
+ final boolean success = container.removeItem(itemId);
+ if (!ordered && success) {
+ removeFromOrderWrapper(itemId);
+ }
+ return success;
+ }
+
+ /**
+ * Removes the specified Property from the underlying container and from the
+ * ordering.
+ * <p>
+ * Note : The Property will be removed from all the Items in the Container.
+ * </p>
+ *
+ * @param propertyId
+ * the ID of the Property to remove.
+ * @return <code>true</code> if the operation succeeded, <code>false</code>
+ * if not
+ * @throws UnsupportedOperationException
+ * if the removeContainerProperty is not supported.
+ */
+ @Override
+ public boolean removeContainerProperty(Object propertyId)
+ throws UnsupportedOperationException {
+ return container.removeContainerProperty(propertyId);
+ }
+
+ /*
+ * Does the container contain the specified Item? Don't add a JavaDoc
+ * comment here, we use the default documentation from implemented
+ * interface.
+ */
+ @Override
+ public boolean containsId(Object itemId) {
+ return container.containsId(itemId);
+ }
+
+ /*
+ * Gets the specified Item from the container. Don't add a JavaDoc comment
+ * here, we use the default documentation from implemented interface.
+ */
+ @Override
+ public Item getItem(Object itemId) {
+ return container.getItem(itemId);
+ }
+
+ /*
+ * Gets the ID's of all Items stored in the Container Don't add a JavaDoc
+ * comment here, we use the default documentation from implemented
+ * interface.
+ */
+ @Override
+ public Collection<?> getItemIds() {
+ return container.getItemIds();
+ }
+
+ /*
+ * Gets the Property identified by the given itemId and propertyId from the
+ * Container Don't add a JavaDoc comment here, we use the default
+ * documentation from implemented interface.
+ */
+ @Override
+ public Property<?> getContainerProperty(Object itemId, Object propertyId) {
+ return container.getContainerProperty(itemId, propertyId);
+ }
+
+ /*
+ * Gets the ID's of all Properties stored in the Container Don't add a
+ * JavaDoc comment here, we use the default documentation from implemented
+ * interface.
+ */
+ @Override
+ public Collection<?> getContainerPropertyIds() {
+ return container.getContainerPropertyIds();
+ }
+
+ /*
+ * Gets the data type of all Properties identified by the given Property ID.
+ * Don't add a JavaDoc comment here, we use the default documentation from
+ * implemented interface.
+ */
+ @Override
+ public Class<?> getType(Object propertyId) {
+ return container.getType(propertyId);
+ }
+
+ /*
+ * Gets the number of Items in the Container. Don't add a JavaDoc comment
+ * here, we use the default documentation from implemented interface.
+ */
+ @Override
+ public int size() {
+ int newSize = container.size();
+ if (lastKnownSize != -1 && newSize != lastKnownSize
+ && !(container instanceof Container.ItemSetChangeNotifier)) {
+ // Update the internal cache when the size of the container changes
+ // and the container is incapable of sending ItemSetChangeEvents
+ updateOrderWrapper();
+ }
+ lastKnownSize = newSize;
+ return newSize;
+ }
+
+ /*
+ * Registers a new Item set change listener for this Container. Don't add a
+ * JavaDoc comment here, we use the default documentation from implemented
+ * interface.
+ */
+ @Override
+ public void addListener(Container.ItemSetChangeListener listener) {
+ if (container instanceof Container.ItemSetChangeNotifier) {
+ ((Container.ItemSetChangeNotifier) container)
+ .addListener(new PiggybackListener(listener));
+ }
+ }
+
+ /*
+ * Removes a Item set change listener from the object. Don't add a JavaDoc
+ * comment here, we use the default documentation from implemented
+ * interface.
+ */
+ @Override
+ public void removeListener(Container.ItemSetChangeListener listener) {
+ if (container instanceof Container.ItemSetChangeNotifier) {
+ ((Container.ItemSetChangeNotifier) container)
+ .removeListener(new PiggybackListener(listener));
+ }
+ }
+
+ /*
+ * Registers a new Property set change listener for this Container. Don't
+ * add a JavaDoc comment here, we use the default documentation from
+ * implemented interface.
+ */
+ @Override
+ public void addListener(Container.PropertySetChangeListener listener) {
+ if (container instanceof Container.PropertySetChangeNotifier) {
+ ((Container.PropertySetChangeNotifier) container)
+ .addListener(new PiggybackListener(listener));
+ }
+ }
+
+ /*
+ * Removes a Property set change listener from the object. Don't add a
+ * JavaDoc comment here, we use the default documentation from implemented
+ * interface.
+ */
+ @Override
+ public void removeListener(Container.PropertySetChangeListener listener) {
+ if (container instanceof Container.PropertySetChangeNotifier) {
+ ((Container.PropertySetChangeNotifier) container)
+ .removeListener(new PiggybackListener(listener));
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container.Ordered#addItemAfter(java.lang.Object,
+ * java.lang.Object)
+ */
+ @Override
+ public Item addItemAfter(Object previousItemId, Object newItemId)
+ throws UnsupportedOperationException {
+
+ // If the previous item is not in the container, fail
+ if (previousItemId != null && !containsId(previousItemId)) {
+ return null;
+ }
+
+ // Adds the item to container
+ final Item item = container.addItem(newItemId);
+
+ // Puts the new item to its correct place
+ if (!ordered && item != null) {
+ addToOrderWrapper(newItemId, previousItemId);
+ }
+
+ return item;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container.Ordered#addItemAfter(java.lang.Object)
+ */
+ @Override
+ public Object addItemAfter(Object previousItemId)
+ throws UnsupportedOperationException {
+
+ // If the previous item is not in the container, fail
+ if (previousItemId != null && !containsId(previousItemId)) {
+ return null;
+ }
+
+ // Adds the item to container
+ final Object id = container.addItem();
+
+ // Puts the new item to its correct place
+ if (!ordered && id != null) {
+ addToOrderWrapper(id, previousItemId);
+ }
+
+ return id;
+ }
+
+ /**
+ * This listener 'piggybacks' on the real listener in order to update the
+ * wrapper when needed. It proxies equals() and hashCode() to the real
+ * listener so that the correct listener gets removed.
+ *
+ */
+ private class PiggybackListener implements
+ Container.PropertySetChangeListener,
+ Container.ItemSetChangeListener {
+
+ Object listener;
+
+ public PiggybackListener(Object realListener) {
+ listener = realListener;
+ }
+
+ @Override
+ public void containerItemSetChange(ItemSetChangeEvent event) {
+ updateOrderWrapper();
+ ((Container.ItemSetChangeListener) listener)
+ .containerItemSetChange(event);
+
+ }
+
+ @Override
+ public void containerPropertySetChange(PropertySetChangeEvent event) {
+ updateOrderWrapper();
+ ((Container.PropertySetChangeListener) listener)
+ .containerPropertySetChange(event);
+
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ return obj == listener || (obj != null && obj.equals(listener));
+ }
+
+ @Override
+ public int hashCode() {
+ return listener.hashCode();
+ }
+
+ }
+
+}
diff --git a/server/src/com/vaadin/data/util/DefaultItemSorter.java b/server/src/com/vaadin/data/util/DefaultItemSorter.java
new file mode 100644
index 0000000000..81b15ebd4f
--- /dev/null
+++ b/server/src/com/vaadin/data/util/DefaultItemSorter.java
@@ -0,0 +1,210 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.List;
+
+import com.vaadin.data.Container;
+import com.vaadin.data.Container.Sortable;
+import com.vaadin.data.Item;
+import com.vaadin.data.Property;
+
+/**
+ * Provides a default implementation of an ItemSorter. The
+ * <code>DefaultItemSorter</code> adheres to the
+ * {@link Sortable#sort(Object[], boolean[])} rules and sorts the container
+ * according to the properties given using
+ * {@link #setSortProperties(Sortable, Object[], boolean[])}.
+ * <p>
+ * A Comparator is used for comparing the individual <code>Property</code>
+ * values. The comparator can be set using the constructor. If no comparator is
+ * provided a default comparator is used.
+ *
+ */
+public class DefaultItemSorter implements ItemSorter {
+
+ private java.lang.Object[] sortPropertyIds;
+ private boolean[] sortDirections;
+ private Container container;
+ private Comparator<Object> propertyValueComparator;
+
+ /**
+ * Constructs a DefaultItemSorter using the default <code>Comparator</code>
+ * for comparing <code>Property</code>values.
+ *
+ */
+ public DefaultItemSorter() {
+ this(new DefaultPropertyValueComparator());
+ }
+
+ /**
+ * Constructs a DefaultItemSorter which uses the <code>Comparator</code>
+ * indicated by the <code>propertyValueComparator</code> parameter for
+ * comparing <code>Property</code>values.
+ *
+ * @param propertyValueComparator
+ * The comparator to use when comparing individual
+ * <code>Property</code> values
+ */
+ public DefaultItemSorter(Comparator<Object> propertyValueComparator) {
+ this.propertyValueComparator = propertyValueComparator;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.util.ItemSorter#compare(java.lang.Object,
+ * java.lang.Object)
+ */
+ @Override
+ public int compare(Object o1, Object o2) {
+ Item item1 = container.getItem(o1);
+ Item item2 = container.getItem(o2);
+
+ /*
+ * Items can be null if the container is filtered. Null is considered
+ * "less" than not-null.
+ */
+ if (item1 == null) {
+ if (item2 == null) {
+ return 0;
+ } else {
+ return 1;
+ }
+ } else if (item2 == null) {
+ return -1;
+ }
+
+ for (int i = 0; i < sortPropertyIds.length; i++) {
+
+ int result = compareProperty(sortPropertyIds[i], sortDirections[i],
+ item1, item2);
+
+ // If order can be decided
+ if (result != 0) {
+ return result;
+ }
+
+ }
+
+ return 0;
+ }
+
+ /**
+ * Compares the property indicated by <code>propertyId</code> in the items
+ * indicated by <code>item1</code> and <code>item2</code> for order. Returns
+ * a negative integer, zero, or a positive integer as the property value in
+ * the first item is less than, equal to, or greater than the property value
+ * in the second item. If the <code>sortDirection</code> is false the
+ * returned value is negated.
+ * <p>
+ * The comparator set for this <code>DefaultItemSorter</code> is used for
+ * comparing the two property values.
+ *
+ * @param propertyId
+ * The property id for the property that is used for comparison.
+ * @param sortDirection
+ * The direction of the sort. A false value negates the result.
+ * @param item1
+ * The first item to compare.
+ * @param item2
+ * The second item to compare.
+ * @return a negative, zero, or positive integer if the property value in
+ * the first item is less than, equal to, or greater than the
+ * property value in the second item. Negated if
+ * {@code sortDirection} is false.
+ */
+ protected int compareProperty(Object propertyId, boolean sortDirection,
+ Item item1, Item item2) {
+
+ // Get the properties to compare
+ final Property<?> property1 = item1.getItemProperty(propertyId);
+ final Property<?> property2 = item2.getItemProperty(propertyId);
+
+ // Get the values to compare
+ final Object value1 = (property1 == null) ? null : property1.getValue();
+ final Object value2 = (property2 == null) ? null : property2.getValue();
+
+ // Result of the comparison
+ int r = 0;
+ if (sortDirection) {
+ r = propertyValueComparator.compare(value1, value2);
+ } else {
+ r = propertyValueComparator.compare(value2, value1);
+ }
+
+ return r;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.data.util.ItemSorter#setSortProperties(com.vaadin.data.Container
+ * .Sortable, java.lang.Object[], boolean[])
+ */
+ @Override
+ public void setSortProperties(Container.Sortable container,
+ Object[] propertyId, boolean[] ascending) {
+ this.container = container;
+
+ // Removes any non-sortable property ids
+ final List<Object> ids = new ArrayList<Object>();
+ final List<Boolean> orders = new ArrayList<Boolean>();
+ final Collection<?> sortable = container
+ .getSortableContainerPropertyIds();
+ for (int i = 0; i < propertyId.length; i++) {
+ if (sortable.contains(propertyId[i])) {
+ ids.add(propertyId[i]);
+ orders.add(Boolean.valueOf(i < ascending.length ? ascending[i]
+ : true));
+ }
+ }
+
+ sortPropertyIds = ids.toArray();
+ sortDirections = new boolean[orders.size()];
+ for (int i = 0; i < sortDirections.length; i++) {
+ sortDirections[i] = (orders.get(i)).booleanValue();
+ }
+
+ }
+
+ /**
+ * Provides a default comparator used for comparing {@link Property} values.
+ * The <code>DefaultPropertyValueComparator</code> assumes all objects it
+ * compares can be cast to Comparable.
+ *
+ */
+ public static class DefaultPropertyValueComparator implements
+ Comparator<Object>, Serializable {
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public int compare(Object o1, Object o2) {
+ int r = 0;
+ // Normal non-null comparison
+ if (o1 != null && o2 != null) {
+ // Assume the objects can be cast to Comparable, throw
+ // ClassCastException otherwise.
+ r = ((Comparable<Object>) o1).compareTo(o2);
+ } else if (o1 == o2) {
+ // Objects are equal if both are null
+ r = 0;
+ } else {
+ if (o1 == null) {
+ r = -1; // null is less than non-null
+ } else {
+ r = 1; // non-null is greater than null
+ }
+ }
+
+ return r;
+ }
+ }
+
+}
diff --git a/server/src/com/vaadin/data/util/FilesystemContainer.java b/server/src/com/vaadin/data/util/FilesystemContainer.java
new file mode 100644
index 0000000000..cdfeb57e14
--- /dev/null
+++ b/server/src/com/vaadin/data/util/FilesystemContainer.java
@@ -0,0 +1,918 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.data.util;
+
+import java.io.File;
+import java.io.FilenameFilter;
+import java.io.IOException;
+import java.io.Serializable;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Date;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+
+import com.vaadin.data.Container;
+import com.vaadin.data.Item;
+import com.vaadin.data.Property;
+import com.vaadin.service.FileTypeResolver;
+import com.vaadin.terminal.Resource;
+
+/**
+ * A hierarchical container wrapper for a filesystem.
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+@SuppressWarnings("serial")
+public class FilesystemContainer implements Container.Hierarchical {
+
+ /**
+ * String identifier of a file's "name" property.
+ */
+ public static String PROPERTY_NAME = "Name";
+
+ /**
+ * String identifier of a file's "size" property.
+ */
+ public static String PROPERTY_SIZE = "Size";
+
+ /**
+ * String identifier of a file's "icon" property.
+ */
+ public static String PROPERTY_ICON = "Icon";
+
+ /**
+ * String identifier of a file's "last modified" property.
+ */
+ public static String PROPERTY_LASTMODIFIED = "Last Modified";
+
+ /**
+ * List of the string identifiers for the available properties.
+ */
+ public static Collection<String> FILE_PROPERTIES;
+
+ private final static Method FILEITEM_LASTMODIFIED;
+
+ private final static Method FILEITEM_NAME;
+
+ private final static Method FILEITEM_ICON;
+
+ private final static Method FILEITEM_SIZE;
+
+ static {
+
+ FILE_PROPERTIES = new ArrayList<String>();
+ FILE_PROPERTIES.add(PROPERTY_NAME);
+ FILE_PROPERTIES.add(PROPERTY_ICON);
+ FILE_PROPERTIES.add(PROPERTY_SIZE);
+ FILE_PROPERTIES.add(PROPERTY_LASTMODIFIED);
+ FILE_PROPERTIES = Collections.unmodifiableCollection(FILE_PROPERTIES);
+ try {
+ FILEITEM_LASTMODIFIED = FileItem.class.getMethod("lastModified",
+ new Class[] {});
+ FILEITEM_NAME = FileItem.class.getMethod("getName", new Class[] {});
+ FILEITEM_ICON = FileItem.class.getMethod("getIcon", new Class[] {});
+ FILEITEM_SIZE = FileItem.class.getMethod("getSize", new Class[] {});
+ } catch (final NoSuchMethodException e) {
+ throw new RuntimeException(
+ "Internal error finding methods in FilesystemContainer");
+ }
+ }
+
+ private File[] roots = new File[] {};
+
+ private FilenameFilter filter = null;
+
+ private boolean recursive = true;
+
+ /**
+ * Constructs a new <code>FileSystemContainer</code> with the specified file
+ * as the root of the filesystem. The files are included recursively.
+ *
+ * @param root
+ * the root file for the new file-system container. Null values
+ * are ignored.
+ */
+ public FilesystemContainer(File root) {
+ if (root != null) {
+ roots = new File[] { root };
+ }
+ }
+
+ /**
+ * Constructs a new <code>FileSystemContainer</code> with the specified file
+ * as the root of the filesystem. The files are included recursively.
+ *
+ * @param root
+ * the root file for the new file-system container.
+ * @param recursive
+ * should the container recursively contain subdirectories.
+ */
+ public FilesystemContainer(File root, boolean recursive) {
+ this(root);
+ setRecursive(recursive);
+ }
+
+ /**
+ * Constructs a new <code>FileSystemContainer</code> with the specified file
+ * as the root of the filesystem.
+ *
+ * @param root
+ * the root file for the new file-system container.
+ * @param extension
+ * the Filename extension (w/o separator) to limit the files in
+ * container.
+ * @param recursive
+ * should the container recursively contain subdirectories.
+ */
+ public FilesystemContainer(File root, String extension, boolean recursive) {
+ this(root);
+ this.setFilter(extension);
+ setRecursive(recursive);
+ }
+
+ /**
+ * Constructs a new <code>FileSystemContainer</code> with the specified root
+ * and recursivity status.
+ *
+ * @param root
+ * the root file for the new file-system container.
+ * @param filter
+ * the Filename filter to limit the files in container.
+ * @param recursive
+ * should the container recursively contain subdirectories.
+ */
+ public FilesystemContainer(File root, FilenameFilter filter,
+ boolean recursive) {
+ this(root);
+ this.setFilter(filter);
+ setRecursive(recursive);
+ }
+
+ /**
+ * Adds new root file directory. Adds a file to be included as root file
+ * directory in the <code>FilesystemContainer</code>.
+ *
+ * @param root
+ * the File to be added as root directory. Null values are
+ * ignored.
+ */
+ public void addRoot(File root) {
+ if (root != null) {
+ final File[] newRoots = new File[roots.length + 1];
+ for (int i = 0; i < roots.length; i++) {
+ newRoots[i] = roots[i];
+ }
+ newRoots[roots.length] = root;
+ roots = newRoots;
+ }
+ }
+
+ /**
+ * Tests if the specified Item in the container may have children. Since a
+ * <code>FileSystemContainer</code> contains files and directories, this
+ * method returns <code>true</code> for directory Items only.
+ *
+ * @param itemId
+ * the id of the item.
+ * @return <code>true</code> if the specified Item is a directory,
+ * <code>false</code> otherwise.
+ */
+ @Override
+ public boolean areChildrenAllowed(Object itemId) {
+ return itemId instanceof File && ((File) itemId).canRead()
+ && ((File) itemId).isDirectory();
+ }
+
+ /*
+ * Gets the ID's of all Items who are children of the specified Item. Don't
+ * add a JavaDoc comment here, we use the default documentation from
+ * implemented interface.
+ */
+ @Override
+ public Collection<File> getChildren(Object itemId) {
+
+ if (!(itemId instanceof File)) {
+ return Collections.unmodifiableCollection(new LinkedList<File>());
+ }
+ File[] f;
+ if (filter != null) {
+ f = ((File) itemId).listFiles(filter);
+ } else {
+ f = ((File) itemId).listFiles();
+ }
+
+ if (f == null) {
+ return Collections.unmodifiableCollection(new LinkedList<File>());
+ }
+
+ final List<File> l = Arrays.asList(f);
+ Collections.sort(l);
+
+ return Collections.unmodifiableCollection(l);
+ }
+
+ /*
+ * Gets the parent item of the specified Item. Don't add a JavaDoc comment
+ * here, we use the default documentation from implemented interface.
+ */
+ @Override
+ public Object getParent(Object itemId) {
+
+ if (!(itemId instanceof File)) {
+ return null;
+ }
+ return ((File) itemId).getParentFile();
+ }
+
+ /*
+ * Tests if the specified Item has any children. Don't add a JavaDoc comment
+ * here, we use the default documentation from implemented interface.
+ */
+ @Override
+ public boolean hasChildren(Object itemId) {
+
+ if (!(itemId instanceof File)) {
+ return false;
+ }
+ String[] l;
+ if (filter != null) {
+ l = ((File) itemId).list(filter);
+ } else {
+ l = ((File) itemId).list();
+ }
+ return (l != null) && (l.length > 0);
+ }
+
+ /*
+ * Tests if the specified Item is the root of the filesystem. Don't add a
+ * JavaDoc comment here, we use the default documentation from implemented
+ * interface.
+ */
+ @Override
+ public boolean isRoot(Object itemId) {
+
+ if (!(itemId instanceof File)) {
+ return false;
+ }
+ for (int i = 0; i < roots.length; i++) {
+ if (roots[i].equals(itemId)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /*
+ * Gets the ID's of all root Items in the container. Don't add a JavaDoc
+ * comment here, we use the default documentation from implemented
+ * interface.
+ */
+ @Override
+ public Collection<File> rootItemIds() {
+
+ File[] f;
+
+ // in single root case we use children
+ if (roots.length == 1) {
+ if (filter != null) {
+ f = roots[0].listFiles(filter);
+ } else {
+ f = roots[0].listFiles();
+ }
+ } else {
+ f = roots;
+ }
+
+ if (f == null) {
+ return Collections.unmodifiableCollection(new LinkedList<File>());
+ }
+
+ final List<File> l = Arrays.asList(f);
+ Collections.sort(l);
+
+ return Collections.unmodifiableCollection(l);
+ }
+
+ /**
+ * Returns <code>false</code> when conversion from files to directories is
+ * not supported.
+ *
+ * @param itemId
+ * the ID of the item.
+ * @param areChildrenAllowed
+ * the boolean value specifying if the Item can have children or
+ * not.
+ * @return <code>true</code> if the operaton is successful otherwise
+ * <code>false</code>.
+ * @throws UnsupportedOperationException
+ * if the setChildrenAllowed is not supported.
+ */
+ @Override
+ public boolean setChildrenAllowed(Object itemId, boolean areChildrenAllowed)
+ throws UnsupportedOperationException {
+
+ throw new UnsupportedOperationException(
+ "Conversion file to/from directory is not supported");
+ }
+
+ /**
+ * Returns <code>false</code> when moving files around in the filesystem is
+ * not supported.
+ *
+ * @param itemId
+ * the ID of the item.
+ * @param newParentId
+ * the ID of the Item that's to be the new parent of the Item
+ * identified with itemId.
+ * @return <code>true</code> if the operation is successful otherwise
+ * <code>false</code>.
+ * @throws UnsupportedOperationException
+ * if the setParent is not supported.
+ */
+ @Override
+ public boolean setParent(Object itemId, Object newParentId)
+ throws UnsupportedOperationException {
+
+ throw new UnsupportedOperationException("File moving is not supported");
+ }
+
+ /*
+ * Tests if the filesystem contains the specified Item. Don't add a JavaDoc
+ * comment here, we use the default documentation from implemented
+ * interface.
+ */
+ @Override
+ public boolean containsId(Object itemId) {
+
+ if (!(itemId instanceof File)) {
+ return false;
+ }
+ boolean val = false;
+
+ // Try to match all roots
+ for (int i = 0; i < roots.length; i++) {
+ try {
+ val |= ((File) itemId).getCanonicalPath().startsWith(
+ roots[i].getCanonicalPath());
+ } catch (final IOException e) {
+ // Exception ignored
+ }
+
+ }
+ if (val && filter != null) {
+ val &= filter.accept(((File) itemId).getParentFile(),
+ ((File) itemId).getName());
+ }
+ return val;
+ }
+
+ /*
+ * Gets the specified Item from the filesystem. Don't add a JavaDoc comment
+ * here, we use the default documentation from implemented interface.
+ */
+ @Override
+ public Item getItem(Object itemId) {
+
+ if (!(itemId instanceof File)) {
+ return null;
+ }
+ return new FileItem((File) itemId);
+ }
+
+ /**
+ * Internal recursive method to add the files under the specified directory
+ * to the collection.
+ *
+ * @param col
+ * the collection where the found items are added
+ * @param f
+ * the root file where to start adding files
+ */
+ private void addItemIds(Collection<File> col, File f) {
+ File[] l;
+ if (filter != null) {
+ l = f.listFiles(filter);
+ } else {
+ l = f.listFiles();
+ }
+ if (l == null) {
+ // File.listFiles returns null if File does not exist or if there
+ // was an IO error (permission denied)
+ return;
+ }
+ final List<File> ll = Arrays.asList(l);
+ Collections.sort(ll);
+
+ for (final Iterator<File> i = ll.iterator(); i.hasNext();) {
+ final File lf = i.next();
+ col.add(lf);
+ if (lf.isDirectory()) {
+ addItemIds(col, lf);
+ }
+ }
+ }
+
+ /*
+ * Gets the IDs of Items in the filesystem. Don't add a JavaDoc comment
+ * here, we use the default documentation from implemented interface.
+ */
+ @Override
+ public Collection<File> getItemIds() {
+
+ if (recursive) {
+ final Collection<File> col = new ArrayList<File>();
+ for (int i = 0; i < roots.length; i++) {
+ addItemIds(col, roots[i]);
+ }
+ return Collections.unmodifiableCollection(col);
+ } else {
+ File[] f;
+ if (roots.length == 1) {
+ if (filter != null) {
+ f = roots[0].listFiles(filter);
+ } else {
+ f = roots[0].listFiles();
+ }
+ } else {
+ f = roots;
+ }
+
+ if (f == null) {
+ return Collections
+ .unmodifiableCollection(new LinkedList<File>());
+ }
+
+ final List<File> l = Arrays.asList(f);
+ Collections.sort(l);
+ return Collections.unmodifiableCollection(l);
+ }
+
+ }
+
+ /**
+ * Gets the specified property of the specified file Item. The available
+ * file properties are "Name", "Size" and "Last Modified". If propertyId is
+ * not one of those, <code>null</code> is returned.
+ *
+ * @param itemId
+ * the ID of the file whose property is requested.
+ * @param propertyId
+ * the property's ID.
+ * @return the requested property's value, or <code>null</code>
+ */
+ @Override
+ public Property<?> getContainerProperty(Object itemId, Object propertyId) {
+
+ if (!(itemId instanceof File)) {
+ return null;
+ }
+
+ if (propertyId.equals(PROPERTY_NAME)) {
+ return new MethodProperty<Object>(getType(propertyId),
+ new FileItem((File) itemId), FILEITEM_NAME, null);
+ }
+
+ if (propertyId.equals(PROPERTY_ICON)) {
+ return new MethodProperty<Object>(getType(propertyId),
+ new FileItem((File) itemId), FILEITEM_ICON, null);
+ }
+
+ if (propertyId.equals(PROPERTY_SIZE)) {
+ return new MethodProperty<Object>(getType(propertyId),
+ new FileItem((File) itemId), FILEITEM_SIZE, null);
+ }
+
+ if (propertyId.equals(PROPERTY_LASTMODIFIED)) {
+ return new MethodProperty<Object>(getType(propertyId),
+ new FileItem((File) itemId), FILEITEM_LASTMODIFIED, null);
+ }
+
+ return null;
+ }
+
+ /**
+ * Gets the collection of available file properties.
+ *
+ * @return Unmodifiable collection containing all available file properties.
+ */
+ @Override
+ public Collection<String> getContainerPropertyIds() {
+ return FILE_PROPERTIES;
+ }
+
+ /**
+ * Gets the specified property's data type. "Name" is a <code>String</code>,
+ * "Size" is a <code>Long</code>, "Last Modified" is a <code>Date</code>. If
+ * propertyId is not one of those, <code>null</code> is returned.
+ *
+ * @param propertyId
+ * the ID of the property whose type is requested.
+ * @return data type of the requested property, or <code>null</code>
+ */
+ @Override
+ public Class<?> getType(Object propertyId) {
+
+ if (propertyId.equals(PROPERTY_NAME)) {
+ return String.class;
+ }
+ if (propertyId.equals(PROPERTY_ICON)) {
+ return Resource.class;
+ }
+ if (propertyId.equals(PROPERTY_SIZE)) {
+ return Long.class;
+ }
+ if (propertyId.equals(PROPERTY_LASTMODIFIED)) {
+ return Date.class;
+ }
+ return null;
+ }
+
+ /**
+ * Internal method to recursively calculate the number of files under a root
+ * directory.
+ *
+ * @param f
+ * the root to start counting from.
+ */
+ private int getFileCounts(File f) {
+ File[] l;
+ if (filter != null) {
+ l = f.listFiles(filter);
+ } else {
+ l = f.listFiles();
+ }
+
+ if (l == null) {
+ return 0;
+ }
+ int ret = l.length;
+ for (int i = 0; i < l.length; i++) {
+ if (l[i].isDirectory()) {
+ ret += getFileCounts(l[i]);
+ }
+ }
+ return ret;
+ }
+
+ /**
+ * Gets the number of Items in the container. In effect, this is the
+ * combined amount of files and directories.
+ *
+ * @return Number of Items in the container.
+ */
+ @Override
+ public int size() {
+
+ if (recursive) {
+ int counts = 0;
+ for (int i = 0; i < roots.length; i++) {
+ counts += getFileCounts(roots[i]);
+ }
+ return counts;
+ } else {
+ File[] f;
+ if (roots.length == 1) {
+ if (filter != null) {
+ f = roots[0].listFiles(filter);
+ } else {
+ f = roots[0].listFiles();
+ }
+ } else {
+ f = roots;
+ }
+
+ if (f == null) {
+ return 0;
+ }
+ return f.length;
+ }
+ }
+
+ /**
+ * A Item wrapper for files in a filesystem.
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+ public class FileItem implements Item {
+
+ /**
+ * The wrapped file.
+ */
+ private final File file;
+
+ /**
+ * Constructs a FileItem from a existing file.
+ */
+ private FileItem(File file) {
+ this.file = file;
+ }
+
+ /*
+ * Gets the specified property of this file. Don't add a JavaDoc comment
+ * here, we use the default documentation from implemented interface.
+ */
+ @Override
+ public Property<?> getItemProperty(Object id) {
+ return getContainerProperty(file, id);
+ }
+
+ /*
+ * Gets the IDs of all properties available for this item Don't add a
+ * JavaDoc comment here, we use the default documentation from
+ * implemented interface.
+ */
+ @Override
+ public Collection<String> getItemPropertyIds() {
+ return getContainerPropertyIds();
+ }
+
+ /**
+ * Calculates a integer hash-code for the Property that's unique inside
+ * the Item containing the Property. Two different Properties inside the
+ * same Item contained in the same list always have different
+ * hash-codes, though Properties in different Items may have identical
+ * hash-codes.
+ *
+ * @return A locally unique hash-code as integer
+ */
+ @Override
+ public int hashCode() {
+ return file.hashCode() ^ FilesystemContainer.this.hashCode();
+ }
+
+ /**
+ * Tests if the given object is the same as the this object. Two
+ * Properties got from an Item with the same ID are equal.
+ *
+ * @param obj
+ * an object to compare with this object.
+ * @return <code>true</code> if the given object is the same as this
+ * object, <code>false</code> if not
+ */
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == null || !(obj instanceof FileItem)) {
+ return false;
+ }
+ final FileItem fi = (FileItem) obj;
+ return fi.getHost() == getHost() && fi.file.equals(file);
+ }
+
+ /**
+ * Gets the host of this file.
+ */
+ private FilesystemContainer getHost() {
+ return FilesystemContainer.this;
+ }
+
+ /**
+ * Gets the last modified date of this file.
+ *
+ * @return Date
+ */
+ public Date lastModified() {
+ return new Date(file.lastModified());
+ }
+
+ /**
+ * Gets the name of this file.
+ *
+ * @return file name of this file.
+ */
+ public String getName() {
+ return file.getName();
+ }
+
+ /**
+ * Gets the icon of this file.
+ *
+ * @return the icon of this file.
+ */
+ public Resource getIcon() {
+ return FileTypeResolver.getIcon(file);
+ }
+
+ /**
+ * Gets the size of this file.
+ *
+ * @return size
+ */
+ public long getSize() {
+ if (file.isDirectory()) {
+ return 0;
+ }
+ return file.length();
+ }
+
+ /**
+ * @see java.lang.Object#toString()
+ */
+ @Override
+ public String toString() {
+ if ("".equals(file.getName())) {
+ return file.getAbsolutePath();
+ }
+ return file.getName();
+ }
+
+ /**
+ * Filesystem container does not support adding new properties.
+ *
+ * @see com.vaadin.data.Item#addItemProperty(Object, Property)
+ */
+ @Override
+ public boolean addItemProperty(Object id, Property property)
+ throws UnsupportedOperationException {
+ throw new UnsupportedOperationException("Filesystem container "
+ + "does not support adding new properties");
+ }
+
+ /**
+ * Filesystem container does not support removing properties.
+ *
+ * @see com.vaadin.data.Item#removeItemProperty(Object)
+ */
+ @Override
+ public boolean removeItemProperty(Object id)
+ throws UnsupportedOperationException {
+ throw new UnsupportedOperationException(
+ "Filesystem container does not support property removal");
+ }
+
+ }
+
+ /**
+ * Generic file extension filter for displaying only files having certain
+ * extension.
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+ public class FileExtensionFilter implements FilenameFilter, Serializable {
+
+ private final String filter;
+
+ /**
+ * Constructs a new FileExtensionFilter using given extension.
+ *
+ * @param fileExtension
+ * the File extension without the separator (dot).
+ */
+ public FileExtensionFilter(String fileExtension) {
+ filter = "." + fileExtension;
+ }
+
+ /**
+ * Allows only files with the extension and directories.
+ *
+ * @see java.io.FilenameFilter#accept(File, String)
+ */
+ @Override
+ public boolean accept(File dir, String name) {
+ if (name.endsWith(filter)) {
+ return true;
+ }
+ return new File(dir, name).isDirectory();
+ }
+
+ }
+
+ /**
+ * Returns the file filter used to limit the files in this container.
+ *
+ * @return Used filter instance or null if no filter is assigned.
+ */
+ public FilenameFilter getFilter() {
+ return filter;
+ }
+
+ /**
+ * Sets the file filter used to limit the files in this container.
+ *
+ * @param filter
+ * The filter to set. <code>null</code> disables filtering.
+ */
+ public void setFilter(FilenameFilter filter) {
+ this.filter = filter;
+ }
+
+ /**
+ * Sets the file filter used to limit the files in this container.
+ *
+ * @param extension
+ * the Filename extension (w/o separator) to limit the files in
+ * container.
+ */
+ public void setFilter(String extension) {
+ filter = new FileExtensionFilter(extension);
+ }
+
+ /**
+ * Is this container recursive filesystem.
+ *
+ * @return <code>true</code> if container is recursive, <code>false</code>
+ * otherwise.
+ */
+ public boolean isRecursive() {
+ return recursive;
+ }
+
+ /**
+ * Sets the container recursive property. Set this to false to limit the
+ * files directly under the root file.
+ * <p>
+ * Note : This is meaningful only if the root really is a directory.
+ * </p>
+ *
+ * @param recursive
+ * the New value for recursive property.
+ */
+ public void setRecursive(boolean recursive) {
+ this.recursive = recursive;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container#addContainerProperty(java.lang.Object,
+ * java.lang.Class, java.lang.Object)
+ */
+ @Override
+ public boolean addContainerProperty(Object propertyId, Class<?> type,
+ Object defaultValue) throws UnsupportedOperationException {
+ throw new UnsupportedOperationException(
+ "File system container does not support this operation");
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container#addItem()
+ */
+ @Override
+ public Object addItem() throws UnsupportedOperationException {
+ throw new UnsupportedOperationException(
+ "File system container does not support this operation");
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container#addItem(java.lang.Object)
+ */
+ @Override
+ public Item addItem(Object itemId) throws UnsupportedOperationException {
+ throw new UnsupportedOperationException(
+ "File system container does not support this operation");
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container#removeAllItems()
+ */
+ @Override
+ public boolean removeAllItems() throws UnsupportedOperationException {
+ throw new UnsupportedOperationException(
+ "File system container does not support this operation");
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container#removeItem(java.lang.Object)
+ */
+ @Override
+ public boolean removeItem(Object itemId)
+ throws UnsupportedOperationException {
+ throw new UnsupportedOperationException(
+ "File system container does not support this operation");
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container#removeContainerProperty(java.lang.Object )
+ */
+ @Override
+ public boolean removeContainerProperty(Object propertyId)
+ throws UnsupportedOperationException {
+ throw new UnsupportedOperationException(
+ "File system container does not support this operation");
+ }
+}
diff --git a/server/src/com/vaadin/data/util/HierarchicalContainer.java b/server/src/com/vaadin/data/util/HierarchicalContainer.java
new file mode 100644
index 0000000000..06ab77c0e7
--- /dev/null
+++ b/server/src/com/vaadin/data/util/HierarchicalContainer.java
@@ -0,0 +1,814 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.data.util;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.LinkedList;
+import java.util.Set;
+
+import com.vaadin.data.Container;
+import com.vaadin.data.Item;
+
+/**
+ * A specialized Container whose contents can be accessed like it was a
+ * tree-like structure.
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+@SuppressWarnings("serial")
+public class HierarchicalContainer extends IndexedContainer implements
+ Container.Hierarchical {
+
+ /**
+ * Set of IDs of those contained Items that can't have children.
+ */
+ private final HashSet<Object> noChildrenAllowed = new HashSet<Object>();
+
+ /**
+ * Mapping from Item ID to parent Item ID.
+ */
+ private final HashMap<Object, Object> parent = new HashMap<Object, Object>();
+
+ /**
+ * Mapping from Item ID to parent Item ID for items included in the filtered
+ * container.
+ */
+ private HashMap<Object, Object> filteredParent = null;
+
+ /**
+ * Mapping from Item ID to a list of child IDs.
+ */
+ private final HashMap<Object, LinkedList<Object>> children = new HashMap<Object, LinkedList<Object>>();
+
+ /**
+ * Mapping from Item ID to a list of child IDs when filtered
+ */
+ private HashMap<Object, LinkedList<Object>> filteredChildren = null;
+
+ /**
+ * List that contains all root elements of the container.
+ */
+ private final LinkedList<Object> roots = new LinkedList<Object>();
+
+ /**
+ * List that contains all filtered root elements of the container.
+ */
+ private LinkedList<Object> filteredRoots = null;
+
+ /**
+ * Determines how filtering of the container is done.
+ */
+ private boolean includeParentsWhenFiltering = true;
+
+ private boolean contentChangedEventsDisabled = false;
+
+ private boolean contentsChangedEventPending;
+
+ /*
+ * Can the specified Item have any children? Don't add a JavaDoc comment
+ * here, we use the default documentation from implemented interface.
+ */
+ @Override
+ public boolean areChildrenAllowed(Object itemId) {
+ if (noChildrenAllowed.contains(itemId)) {
+ return false;
+ }
+ return containsId(itemId);
+ }
+
+ /*
+ * Gets the IDs of the children of the specified Item. Don't add a JavaDoc
+ * comment here, we use the default documentation from implemented
+ * interface.
+ */
+ @Override
+ public Collection<?> getChildren(Object itemId) {
+ LinkedList<Object> c;
+
+ if (filteredChildren != null) {
+ c = filteredChildren.get(itemId);
+ } else {
+ c = children.get(itemId);
+ }
+
+ if (c == null) {
+ return null;
+ }
+ return Collections.unmodifiableCollection(c);
+ }
+
+ /*
+ * Gets the ID of the parent of the specified Item. Don't add a JavaDoc
+ * comment here, we use the default documentation from implemented
+ * interface.
+ */
+ @Override
+ public Object getParent(Object itemId) {
+ if (filteredParent != null) {
+ return filteredParent.get(itemId);
+ }
+ return parent.get(itemId);
+ }
+
+ /*
+ * Is the Item corresponding to the given ID a leaf node? Don't add a
+ * JavaDoc comment here, we use the default documentation from implemented
+ * interface.
+ */
+ @Override
+ public boolean hasChildren(Object itemId) {
+ if (filteredChildren != null) {
+ return filteredChildren.containsKey(itemId);
+ } else {
+ return children.containsKey(itemId);
+ }
+ }
+
+ /*
+ * Is the Item corresponding to the given ID a root node? Don't add a
+ * JavaDoc comment here, we use the default documentation from implemented
+ * interface.
+ */
+ @Override
+ public boolean isRoot(Object itemId) {
+ // If the container is filtered the itemId must be among filteredRoots
+ // to be a root.
+ if (filteredRoots != null) {
+ if (!filteredRoots.contains(itemId)) {
+ return false;
+ }
+ } else {
+ // Container is not filtered
+ if (parent.containsKey(itemId)) {
+ return false;
+ }
+ }
+
+ return containsId(itemId);
+ }
+
+ /*
+ * Gets the IDs of the root elements in the container. Don't add a JavaDoc
+ * comment here, we use the default documentation from implemented
+ * interface.
+ */
+ @Override
+ public Collection<?> rootItemIds() {
+ if (filteredRoots != null) {
+ return Collections.unmodifiableCollection(filteredRoots);
+ } else {
+ return Collections.unmodifiableCollection(roots);
+ }
+ }
+
+ /**
+ * <p>
+ * Sets the given Item's capability to have children. If the Item identified
+ * with the itemId already has children and the areChildrenAllowed is false
+ * this method fails and <code>false</code> is returned; the children must
+ * be first explicitly removed with
+ * {@link #setParent(Object itemId, Object newParentId)} or
+ * {@link com.vaadin.data.Container#removeItem(Object itemId)}.
+ * </p>
+ *
+ * @param itemId
+ * the ID of the Item in the container whose child capability is
+ * to be set.
+ * @param childrenAllowed
+ * the boolean value specifying if the Item can have children or
+ * not.
+ * @return <code>true</code> if the operation succeeded, <code>false</code>
+ * if not
+ */
+ @Override
+ public boolean setChildrenAllowed(Object itemId, boolean childrenAllowed) {
+
+ // Checks that the item is in the container
+ if (!containsId(itemId)) {
+ return false;
+ }
+
+ // Updates status
+ if (childrenAllowed) {
+ noChildrenAllowed.remove(itemId);
+ } else {
+ noChildrenAllowed.add(itemId);
+ }
+
+ return true;
+ }
+
+ /**
+ * <p>
+ * Sets the parent of an Item. The new parent item must exist and be able to
+ * have children. (<code>canHaveChildren(newParentId) == true</code>). It is
+ * also possible to detach a node from the hierarchy (and thus make it root)
+ * by setting the parent <code>null</code>.
+ * </p>
+ *
+ * @param itemId
+ * the ID of the item to be set as the child of the Item
+ * identified with newParentId.
+ * @param newParentId
+ * the ID of the Item that's to be the new parent of the Item
+ * identified with itemId.
+ * @return <code>true</code> if the operation succeeded, <code>false</code>
+ * if not
+ */
+ @Override
+ public boolean setParent(Object itemId, Object newParentId) {
+
+ // Checks that the item is in the container
+ if (!containsId(itemId)) {
+ return false;
+ }
+
+ // Gets the old parent
+ final Object oldParentId = parent.get(itemId);
+
+ // Checks if no change is necessary
+ if ((newParentId == null && oldParentId == null)
+ || ((newParentId != null) && newParentId.equals(oldParentId))) {
+ return true;
+ }
+
+ // Making root?
+ if (newParentId == null) {
+ // The itemId should become a root so we need to
+ // - Remove it from the old parent's children list
+ // - Add it as a root
+ // - Remove it from the item -> parent list (parent is null for
+ // roots)
+
+ // Removes from old parents children list
+ final LinkedList<Object> l = children.get(oldParentId);
+ if (l != null) {
+ l.remove(itemId);
+ if (l.isEmpty()) {
+ children.remove(oldParentId);
+ }
+
+ }
+
+ // Add to be a root
+ roots.add(itemId);
+
+ // Updates parent
+ parent.remove(itemId);
+
+ if (hasFilters()) {
+ // Refilter the container if setParent is called when filters
+ // are applied. Changing parent can change what is included in
+ // the filtered version (if includeParentsWhenFiltering==true).
+ doFilterContainer(hasFilters());
+ }
+
+ fireItemSetChange();
+
+ return true;
+ }
+
+ // We get here when the item should not become a root and we need to
+ // - Verify the new parent exists and can have children
+ // - Check that the new parent is not a child of the selected itemId
+ // - Updated the item -> parent mapping to point to the new parent
+ // - Remove the item from the roots list if it was a root
+ // - Remove the item from the old parent's children list if it was not a
+ // root
+
+ // Checks that the new parent exists in container and can have
+ // children
+ if (!containsId(newParentId) || noChildrenAllowed.contains(newParentId)) {
+ return false;
+ }
+
+ // Checks that setting parent doesn't result to a loop
+ Object o = newParentId;
+ while (o != null && !o.equals(itemId)) {
+ o = parent.get(o);
+ }
+ if (o != null) {
+ return false;
+ }
+
+ // Updates parent
+ parent.put(itemId, newParentId);
+ LinkedList<Object> pcl = children.get(newParentId);
+ if (pcl == null) {
+ // Create an empty list for holding children if one were not
+ // previously created
+ pcl = new LinkedList<Object>();
+ children.put(newParentId, pcl);
+ }
+ pcl.add(itemId);
+
+ // Removes from old parent or root
+ if (oldParentId == null) {
+ roots.remove(itemId);
+ } else {
+ final LinkedList<Object> l = children.get(oldParentId);
+ if (l != null) {
+ l.remove(itemId);
+ if (l.isEmpty()) {
+ children.remove(oldParentId);
+ }
+ }
+ }
+
+ if (hasFilters()) {
+ // Refilter the container if setParent is called when filters
+ // are applied. Changing parent can change what is included in
+ // the filtered version (if includeParentsWhenFiltering==true).
+ doFilterContainer(hasFilters());
+ }
+
+ fireItemSetChange();
+
+ return true;
+ }
+
+ private boolean hasFilters() {
+ return (filteredRoots != null);
+ }
+
+ /**
+ * Moves a node (an Item) in the container immediately after a sibling node.
+ * The two nodes must have the same parent in the container.
+ *
+ * @param itemId
+ * the identifier of the moved node (Item)
+ * @param siblingId
+ * the identifier of the reference node (Item), after which the
+ * other node will be located
+ */
+ public void moveAfterSibling(Object itemId, Object siblingId) {
+ Object parent2 = getParent(itemId);
+ LinkedList<Object> childrenList;
+ if (parent2 == null) {
+ childrenList = roots;
+ } else {
+ childrenList = children.get(parent2);
+ }
+ if (siblingId == null) {
+ childrenList.remove(itemId);
+ childrenList.addFirst(itemId);
+
+ } else {
+ int oldIndex = childrenList.indexOf(itemId);
+ int indexOfSibling = childrenList.indexOf(siblingId);
+ if (indexOfSibling != -1 && oldIndex != -1) {
+ int newIndex;
+ if (oldIndex > indexOfSibling) {
+ newIndex = indexOfSibling + 1;
+ } else {
+ newIndex = indexOfSibling;
+ }
+ childrenList.remove(oldIndex);
+ childrenList.add(newIndex, itemId);
+ } else {
+ throw new IllegalArgumentException(
+ "Given identifiers no not have the same parent.");
+ }
+ }
+ fireItemSetChange();
+
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.util.IndexedContainer#addItem()
+ */
+ @Override
+ public Object addItem() {
+ disableContentsChangeEvents();
+ final Object itemId = super.addItem();
+ if (itemId == null) {
+ return null;
+ }
+
+ if (!roots.contains(itemId)) {
+ roots.add(itemId);
+ if (filteredRoots != null) {
+ if (passesFilters(itemId)) {
+ filteredRoots.add(itemId);
+ }
+ }
+ }
+ enableAndFireContentsChangeEvents();
+ return itemId;
+ }
+
+ @Override
+ protected void fireItemSetChange(
+ com.vaadin.data.Container.ItemSetChangeEvent event) {
+ if (contentsChangeEventsOn()) {
+ super.fireItemSetChange(event);
+ } else {
+ contentsChangedEventPending = true;
+ }
+ }
+
+ private boolean contentsChangeEventsOn() {
+ return !contentChangedEventsDisabled;
+ }
+
+ private void disableContentsChangeEvents() {
+ contentChangedEventsDisabled = true;
+ }
+
+ private void enableAndFireContentsChangeEvents() {
+ contentChangedEventsDisabled = false;
+ if (contentsChangedEventPending) {
+ fireItemSetChange();
+ }
+ contentsChangedEventPending = false;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.util.IndexedContainer#addItem(java.lang.Object)
+ */
+ @Override
+ public Item addItem(Object itemId) {
+ disableContentsChangeEvents();
+ final Item item = super.addItem(itemId);
+ if (item == null) {
+ return null;
+ }
+
+ roots.add(itemId);
+
+ if (filteredRoots != null) {
+ if (passesFilters(itemId)) {
+ filteredRoots.add(itemId);
+ }
+ }
+ enableAndFireContentsChangeEvents();
+ return item;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.util.IndexedContainer#removeAllItems()
+ */
+ @Override
+ public boolean removeAllItems() {
+ disableContentsChangeEvents();
+ final boolean success = super.removeAllItems();
+
+ if (success) {
+ roots.clear();
+ parent.clear();
+ children.clear();
+ noChildrenAllowed.clear();
+ if (filteredRoots != null) {
+ filteredRoots = null;
+ }
+ if (filteredChildren != null) {
+ filteredChildren = null;
+ }
+ if (filteredParent != null) {
+ filteredParent = null;
+ }
+ }
+ enableAndFireContentsChangeEvents();
+ return success;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.util.IndexedContainer#removeItem(java.lang.Object )
+ */
+ @Override
+ public boolean removeItem(Object itemId) {
+ disableContentsChangeEvents();
+ final boolean success = super.removeItem(itemId);
+
+ if (success) {
+ // Remove from roots if this was a root
+ if (roots.remove(itemId)) {
+
+ // If filtering is enabled we might need to remove it from the
+ // filtered list also
+ if (filteredRoots != null) {
+ filteredRoots.remove(itemId);
+ }
+ }
+
+ // Clear the children list. Old children will now become root nodes
+ LinkedList<Object> childNodeIds = children.remove(itemId);
+ if (childNodeIds != null) {
+ if (filteredChildren != null) {
+ filteredChildren.remove(itemId);
+ }
+ for (Object childId : childNodeIds) {
+ setParent(childId, null);
+ }
+ }
+
+ // Parent of the item that we are removing will contain the item id
+ // in its children list
+ final Object parentItemId = parent.get(itemId);
+ if (parentItemId != null) {
+ final LinkedList<Object> c = children.get(parentItemId);
+ if (c != null) {
+ c.remove(itemId);
+
+ if (c.isEmpty()) {
+ children.remove(parentItemId);
+ }
+
+ // Found in the children list so might also be in the
+ // filteredChildren list
+ if (filteredChildren != null) {
+ LinkedList<Object> f = filteredChildren
+ .get(parentItemId);
+ if (f != null) {
+ f.remove(itemId);
+ if (f.isEmpty()) {
+ filteredChildren.remove(parentItemId);
+ }
+ }
+ }
+ }
+ }
+ parent.remove(itemId);
+ if (filteredParent != null) {
+ // Item id no longer has a parent as the item id is not in the
+ // container.
+ filteredParent.remove(itemId);
+ }
+ noChildrenAllowed.remove(itemId);
+ }
+
+ enableAndFireContentsChangeEvents();
+
+ return success;
+ }
+
+ /**
+ * Removes the Item identified by given itemId and all its children.
+ *
+ * @see #removeItem(Object)
+ * @param itemId
+ * the identifier of the Item to be removed
+ * @return true if the operation succeeded
+ */
+ public boolean removeItemRecursively(Object itemId) {
+ disableContentsChangeEvents();
+ boolean removeItemRecursively = removeItemRecursively(this, itemId);
+ enableAndFireContentsChangeEvents();
+ return removeItemRecursively;
+ }
+
+ /**
+ * Removes the Item identified by given itemId and all its children from the
+ * given Container.
+ *
+ * @param container
+ * the container where the item is to be removed
+ * @param itemId
+ * the identifier of the Item to be removed
+ * @return true if the operation succeeded
+ */
+ public static boolean removeItemRecursively(
+ Container.Hierarchical container, Object itemId) {
+ boolean success = true;
+ Collection<?> children2 = container.getChildren(itemId);
+ if (children2 != null) {
+ Object[] array = children2.toArray();
+ for (int i = 0; i < array.length; i++) {
+ boolean removeItemRecursively = removeItemRecursively(
+ container, array[i]);
+ if (!removeItemRecursively) {
+ success = false;
+ }
+ }
+ }
+ // remove the root of subtree if children where succesfully removed
+ if (success) {
+ success = container.removeItem(itemId);
+ }
+ return success;
+
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.util.IndexedContainer#doSort()
+ */
+ @Override
+ protected void doSort() {
+ super.doSort();
+
+ Collections.sort(roots, getItemSorter());
+ for (LinkedList<Object> childList : children.values()) {
+ Collections.sort(childList, getItemSorter());
+ }
+ }
+
+ /**
+ * Used to control how filtering works. @see
+ * {@link #setIncludeParentsWhenFiltering(boolean)} for more information.
+ *
+ * @return true if all parents for items that match the filter are included
+ * when filtering, false if only the matching items are included
+ */
+ public boolean isIncludeParentsWhenFiltering() {
+ return includeParentsWhenFiltering;
+ }
+
+ /**
+ * Controls how the filtering of the container works. Set this to true to
+ * make filtering include parents for all matched items in addition to the
+ * items themselves. Setting this to false causes the filtering to only
+ * include the matching items and make items with excluded parents into root
+ * items.
+ *
+ * @param includeParentsWhenFiltering
+ * true to include all parents for items that match the filter,
+ * false to only include the matching items
+ */
+ public void setIncludeParentsWhenFiltering(
+ boolean includeParentsWhenFiltering) {
+ this.includeParentsWhenFiltering = includeParentsWhenFiltering;
+ if (filteredRoots != null) {
+ // Currently filtered so needs to be re-filtered
+ doFilterContainer(true);
+ }
+ }
+
+ /*
+ * Overridden to provide filtering for root & children items.
+ *
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.util.IndexedContainer#updateContainerFiltering()
+ */
+ @Override
+ protected boolean doFilterContainer(boolean hasFilters) {
+ if (!hasFilters) {
+ // All filters removed
+ filteredRoots = null;
+ filteredChildren = null;
+ filteredParent = null;
+
+ return super.doFilterContainer(hasFilters);
+ }
+
+ // Reset data structures
+ filteredRoots = new LinkedList<Object>();
+ filteredChildren = new HashMap<Object, LinkedList<Object>>();
+ filteredParent = new HashMap<Object, Object>();
+
+ if (includeParentsWhenFiltering) {
+ // Filter so that parents for items that match the filter are also
+ // included
+ HashSet<Object> includedItems = new HashSet<Object>();
+ for (Object rootId : roots) {
+ if (filterIncludingParents(rootId, includedItems)) {
+ filteredRoots.add(rootId);
+ addFilteredChildrenRecursively(rootId, includedItems);
+ }
+ }
+ // includedItemIds now contains all the item ids that should be
+ // included. Filter IndexedContainer based on this
+ filterOverride = includedItems;
+ super.doFilterContainer(hasFilters);
+ filterOverride = null;
+
+ return true;
+ } else {
+ // Filter by including all items that pass the filter and make items
+ // with no parent new root items
+
+ // Filter IndexedContainer first so getItemIds return the items that
+ // match
+ super.doFilterContainer(hasFilters);
+
+ LinkedHashSet<Object> filteredItemIds = new LinkedHashSet<Object>(
+ getItemIds());
+
+ for (Object itemId : filteredItemIds) {
+ Object itemParent = parent.get(itemId);
+ if (itemParent == null || !filteredItemIds.contains(itemParent)) {
+ // Parent is not included or this was a root, in both cases
+ // this should be a filtered root
+ filteredRoots.add(itemId);
+ } else {
+ // Parent is included. Add this to the children list (create
+ // it first if necessary)
+ addFilteredChild(itemParent, itemId);
+ }
+ }
+
+ return true;
+ }
+ }
+
+ /**
+ * Adds the given childItemId as a filteredChildren for the parentItemId and
+ * sets it filteredParent.
+ *
+ * @param parentItemId
+ * @param childItemId
+ */
+ private void addFilteredChild(Object parentItemId, Object childItemId) {
+ LinkedList<Object> parentToChildrenList = filteredChildren
+ .get(parentItemId);
+ if (parentToChildrenList == null) {
+ parentToChildrenList = new LinkedList<Object>();
+ filteredChildren.put(parentItemId, parentToChildrenList);
+ }
+ filteredParent.put(childItemId, parentItemId);
+ parentToChildrenList.add(childItemId);
+
+ }
+
+ /**
+ * Recursively adds all items in the includedItems list to the
+ * filteredChildren map in the same order as they are in the children map.
+ * Starts from parentItemId and recurses down as long as child items that
+ * should be included are found.
+ *
+ * @param parentItemId
+ * The item id to start recurse from. Not added to a
+ * filteredChildren list
+ * @param includedItems
+ * Set containing the item ids for the items that should be
+ * included in the filteredChildren map
+ */
+ private void addFilteredChildrenRecursively(Object parentItemId,
+ HashSet<Object> includedItems) {
+ LinkedList<Object> childList = children.get(parentItemId);
+ if (childList == null) {
+ return;
+ }
+
+ for (Object childItemId : childList) {
+ if (includedItems.contains(childItemId)) {
+ addFilteredChild(parentItemId, childItemId);
+ addFilteredChildrenRecursively(childItemId, includedItems);
+ }
+ }
+ }
+
+ /**
+ * Scans the itemId and all its children for which items should be included
+ * when filtering. All items which passes the filters are included.
+ * Additionally all items that have a child node that should be included are
+ * also themselves included.
+ *
+ * @param itemId
+ * @param includedItems
+ * @return true if the itemId should be included in the filtered container.
+ */
+ private boolean filterIncludingParents(Object itemId,
+ HashSet<Object> includedItems) {
+ boolean toBeIncluded = passesFilters(itemId);
+
+ LinkedList<Object> childList = children.get(itemId);
+ if (childList != null) {
+ for (Object childItemId : children.get(itemId)) {
+ toBeIncluded |= filterIncludingParents(childItemId,
+ includedItems);
+ }
+ }
+
+ if (toBeIncluded) {
+ includedItems.add(itemId);
+ }
+ return toBeIncluded;
+ }
+
+ private Set<Object> filterOverride = null;
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.data.util.IndexedContainer#passesFilters(java.lang.Object)
+ */
+ @Override
+ protected boolean passesFilters(Object itemId) {
+ if (filterOverride != null) {
+ return filterOverride.contains(itemId);
+ } else {
+ return super.passesFilters(itemId);
+ }
+ }
+}
diff --git a/server/src/com/vaadin/data/util/HierarchicalContainerOrderedWrapper.java b/server/src/com/vaadin/data/util/HierarchicalContainerOrderedWrapper.java
new file mode 100644
index 0000000000..172dc0dd4f
--- /dev/null
+++ b/server/src/com/vaadin/data/util/HierarchicalContainerOrderedWrapper.java
@@ -0,0 +1,70 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util;
+
+import java.util.Collection;
+
+import com.vaadin.data.Container.Hierarchical;
+
+/**
+ * A wrapper class for adding external ordering to containers not implementing
+ * the {@link com.vaadin.data.Container.Ordered} interface while retaining
+ * {@link Hierarchical} features.
+ *
+ * @see ContainerOrderedWrapper
+ */
+@SuppressWarnings({ "serial" })
+public class HierarchicalContainerOrderedWrapper extends
+ ContainerOrderedWrapper implements Hierarchical {
+
+ private Hierarchical hierarchical;
+
+ public HierarchicalContainerOrderedWrapper(Hierarchical toBeWrapped) {
+ super(toBeWrapped);
+ hierarchical = toBeWrapped;
+ }
+
+ @Override
+ public boolean areChildrenAllowed(Object itemId) {
+ return hierarchical.areChildrenAllowed(itemId);
+ }
+
+ @Override
+ public Collection<?> getChildren(Object itemId) {
+ return hierarchical.getChildren(itemId);
+ }
+
+ @Override
+ public Object getParent(Object itemId) {
+ return hierarchical.getParent(itemId);
+ }
+
+ @Override
+ public boolean hasChildren(Object itemId) {
+ return hierarchical.hasChildren(itemId);
+ }
+
+ @Override
+ public boolean isRoot(Object itemId) {
+ return hierarchical.isRoot(itemId);
+ }
+
+ @Override
+ public Collection<?> rootItemIds() {
+ return hierarchical.rootItemIds();
+ }
+
+ @Override
+ public boolean setChildrenAllowed(Object itemId, boolean areChildrenAllowed)
+ throws UnsupportedOperationException {
+ return hierarchical.setChildrenAllowed(itemId, areChildrenAllowed);
+ }
+
+ @Override
+ public boolean setParent(Object itemId, Object newParentId)
+ throws UnsupportedOperationException {
+ return hierarchical.setParent(itemId, newParentId);
+ }
+
+}
diff --git a/server/src/com/vaadin/data/util/IndexedContainer.java b/server/src/com/vaadin/data/util/IndexedContainer.java
new file mode 100644
index 0000000000..b95b2c4de8
--- /dev/null
+++ b/server/src/com/vaadin/data/util/IndexedContainer.java
@@ -0,0 +1,1109 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.data.util;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.EventObject;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Hashtable;
+import java.util.Iterator;
+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.SimpleStringFilter;
+import com.vaadin.data.util.filter.UnsupportedFilterException;
+
+/**
+ * An implementation of the <code>{@link Container.Indexed}</code> interface
+ * with all important features.</p>
+ *
+ * Features:
+ * <ul>
+ * <li> {@link Container.Indexed}
+ * <li> {@link Container.Ordered}
+ * <li> {@link Container.Sortable}
+ * <li> {@link Container.Filterable}
+ * <li> {@link Cloneable} (deprecated, might be removed in the future)
+ * <li>Sends all needed events on content changes.
+ * </ul>
+ *
+ * @see com.vaadin.data.Container
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+
+@SuppressWarnings("serial")
+// item type is really IndexedContainerItem, but using Item not to show it in
+// public API
+public class IndexedContainer extends
+ AbstractInMemoryContainer<Object, Object, Item> implements
+ Container.PropertySetChangeNotifier, Property.ValueChangeNotifier,
+ Container.Sortable, Cloneable, Container.Filterable,
+ Container.SimpleFilterable {
+
+ /* Internal structure */
+
+ /**
+ * Linked list of ordered Property IDs.
+ */
+ private ArrayList<Object> propertyIds = new ArrayList<Object>();
+
+ /**
+ * Property ID to type mapping.
+ */
+ private Hashtable<Object, Class<?>> types = new Hashtable<Object, Class<?>>();
+
+ /**
+ * Hash of Items, where each Item is implemented as a mapping from Property
+ * ID to Property value.
+ */
+ private Hashtable<Object, Map<Object, Object>> items = new Hashtable<Object, Map<Object, Object>>();
+
+ /**
+ * Set of properties that are read-only.
+ */
+ private HashSet<Property<?>> readOnlyProperties = new HashSet<Property<?>>();
+
+ /**
+ * List of all Property value change event listeners listening all the
+ * properties.
+ */
+ private LinkedList<Property.ValueChangeListener> propertyValueChangeListeners = null;
+
+ /**
+ * Data structure containing all listeners interested in changes to single
+ * Properties. The data structure is a hashtable mapping Property IDs to a
+ * hashtable that maps Item IDs to a linked list of listeners listening
+ * Property identified by given Property ID and Item ID.
+ */
+ private Hashtable<Object, Map<Object, List<Property.ValueChangeListener>>> singlePropertyValueChangeListeners = null;
+
+ private HashMap<Object, Object> defaultPropertyValues;
+
+ private int nextGeneratedItemId = 1;
+
+ /* Container constructors */
+
+ public IndexedContainer() {
+ super();
+ }
+
+ public IndexedContainer(Collection<?> itemIds) {
+ this();
+ if (items != null) {
+ for (final Iterator<?> i = itemIds.iterator(); i.hasNext();) {
+ Object itemId = i.next();
+ internalAddItemAtEnd(itemId, new IndexedContainerItem(itemId),
+ false);
+ }
+ filterAll();
+ }
+ }
+
+ /* Container methods */
+
+ @Override
+ protected Item getUnfilteredItem(Object itemId) {
+ if (itemId != null && items.containsKey(itemId)) {
+ return new IndexedContainerItem(itemId);
+ }
+ return null;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container#getContainerPropertyIds()
+ */
+ @Override
+ public Collection<?> getContainerPropertyIds() {
+ return Collections.unmodifiableCollection(propertyIds);
+ }
+
+ /**
+ * Gets the type of a Property stored in the list.
+ *
+ * @param id
+ * the ID of the Property.
+ * @return Type of the requested Property
+ */
+ @Override
+ public Class<?> getType(Object propertyId) {
+ return types.get(propertyId);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container#getContainerProperty(java.lang.Object,
+ * java.lang.Object)
+ */
+ @Override
+ public Property<?> getContainerProperty(Object itemId, Object propertyId) {
+ if (!containsId(itemId)) {
+ return null;
+ }
+
+ return new IndexedContainerProperty(itemId, propertyId);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container#addContainerProperty(java.lang.Object,
+ * java.lang.Class, java.lang.Object)
+ */
+ @Override
+ public boolean addContainerProperty(Object propertyId, Class<?> type,
+ Object defaultValue) {
+
+ // Fails, if nulls are given
+ if (propertyId == null || type == null) {
+ return false;
+ }
+
+ // Fails if the Property is already present
+ if (propertyIds.contains(propertyId)) {
+ return false;
+ }
+
+ // Adds the Property to Property list and types
+ propertyIds.add(propertyId);
+ types.put(propertyId, type);
+
+ // If default value is given, set it
+ if (defaultValue != null) {
+ // for existing rows
+ for (final Iterator<?> i = getAllItemIds().iterator(); i.hasNext();) {
+ getItem(i.next()).getItemProperty(propertyId).setValue(
+ defaultValue);
+ }
+ // store for next rows
+ if (defaultPropertyValues == null) {
+ defaultPropertyValues = new HashMap<Object, Object>();
+ }
+ defaultPropertyValues.put(propertyId, defaultValue);
+ }
+
+ // Sends a change event
+ fireContainerPropertySetChange();
+
+ return true;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container#removeAllItems()
+ */
+ @Override
+ public boolean removeAllItems() {
+ int origSize = size();
+
+ internalRemoveAllItems();
+
+ items.clear();
+
+ // fire event only if the visible view changed, regardless of whether
+ // filtered out items were removed or not
+ if (origSize != 0) {
+ // Sends a change event
+ fireItemSetChange();
+ }
+
+ return true;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container#addItem()
+ */
+ @Override
+ public Object addItem() {
+
+ // Creates a new id
+ final Object id = generateId();
+
+ // Adds the Item into container
+ addItem(id);
+
+ return id;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container#addItem(java.lang.Object)
+ */
+ @Override
+ public Item addItem(Object itemId) {
+ Item item = internalAddItemAtEnd(itemId, new IndexedContainerItem(
+ itemId), false);
+ if (!isFiltered()) {
+ // always the last item
+ fireItemAdded(size() - 1, itemId, item);
+ } else if (passesFilters(itemId) && !containsId(itemId)) {
+ getFilteredItemIds().add(itemId);
+ // always the last item
+ fireItemAdded(size() - 1, itemId, item);
+ }
+ return item;
+ }
+
+ /**
+ * Helper method to add default values for items if available
+ *
+ * @param t
+ * data table of added item
+ */
+ private void addDefaultValues(Hashtable<Object, Object> t) {
+ if (defaultPropertyValues != null) {
+ for (Object key : defaultPropertyValues.keySet()) {
+ t.put(key, defaultPropertyValues.get(key));
+ }
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container#removeItem(java.lang.Object)
+ */
+ @Override
+ public boolean removeItem(Object itemId) {
+ if (itemId == null || items.remove(itemId) == null) {
+ return false;
+ }
+ int origSize = size();
+ int position = indexOfId(itemId);
+ if (internalRemoveItem(itemId)) {
+ // fire event only if the visible view changed, regardless of
+ // whether filtered out items were removed or not
+ if (size() != origSize) {
+ fireItemRemoved(position, itemId);
+ }
+
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container#removeContainerProperty(java.lang.Object )
+ */
+ @Override
+ public boolean removeContainerProperty(Object propertyId) {
+
+ // Fails if the Property is not present
+ if (!propertyIds.contains(propertyId)) {
+ return false;
+ }
+
+ // Removes the Property to Property list and types
+ propertyIds.remove(propertyId);
+ types.remove(propertyId);
+ if (defaultPropertyValues != null) {
+ defaultPropertyValues.remove(propertyId);
+ }
+
+ // If remove the Property from all Items
+ for (final Iterator<Object> i = getAllItemIds().iterator(); i.hasNext();) {
+ items.get(i.next()).remove(propertyId);
+ }
+
+ // Sends a change event
+ fireContainerPropertySetChange();
+
+ return true;
+ }
+
+ /* Container.Ordered methods */
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container.Ordered#addItemAfter(java.lang.Object,
+ * java.lang.Object)
+ */
+ @Override
+ public Item addItemAfter(Object previousItemId, Object newItemId) {
+ return internalAddItemAfter(previousItemId, newItemId,
+ new IndexedContainerItem(newItemId), true);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container.Ordered#addItemAfter(java.lang.Object)
+ */
+ @Override
+ public Object addItemAfter(Object previousItemId) {
+
+ // Creates a new id
+ final Object id = generateId();
+
+ if (addItemAfter(previousItemId, id) != null) {
+ return id;
+ } else {
+ return null;
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container.Indexed#addItemAt(int, java.lang.Object)
+ */
+ @Override
+ public Item addItemAt(int index, Object newItemId) {
+ return internalAddItemAt(index, newItemId, new IndexedContainerItem(
+ newItemId), true);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container.Indexed#addItemAt(int)
+ */
+ @Override
+ public Object addItemAt(int index) {
+
+ // Creates a new id
+ final Object id = generateId();
+
+ // Adds the Item into container
+ addItemAt(index, id);
+
+ return id;
+ }
+
+ /**
+ * Generates an unique identifier for use as an item id. Guarantees that the
+ * generated id is not currently used as an id.
+ *
+ * @return
+ */
+ private Serializable generateId() {
+ Serializable id;
+ do {
+ id = Integer.valueOf(nextGeneratedItemId++);
+ } while (items.containsKey(id));
+
+ return id;
+ }
+
+ @Override
+ protected void registerNewItem(int index, Object newItemId, Item item) {
+ Hashtable<Object, Object> t = new Hashtable<Object, Object>();
+ items.put(newItemId, t);
+ addDefaultValues(t);
+ }
+
+ /* Event notifiers */
+
+ /**
+ * An <code>event</code> object specifying the list whose Item set has
+ * changed.
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+ public static class ItemSetChangeEvent extends BaseItemSetChangeEvent {
+
+ private final int addedItemIndex;
+
+ private ItemSetChangeEvent(IndexedContainer source, int addedItemIndex) {
+ super(source);
+ this.addedItemIndex = addedItemIndex;
+ }
+
+ /**
+ * Iff one item is added, gives its index.
+ *
+ * @return -1 if either multiple items are changed or some other change
+ * than add is done.
+ */
+ public int getAddedItemIndex() {
+ return addedItemIndex;
+ }
+
+ }
+
+ /**
+ * An <code>event</code> object specifying the Property in a list whose
+ * value has changed.
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+ private static class PropertyValueChangeEvent extends EventObject implements
+ Property.ValueChangeEvent, Serializable {
+
+ private PropertyValueChangeEvent(Property source) {
+ super(source);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Property.ValueChangeEvent#getProperty()
+ */
+ @Override
+ public Property getProperty() {
+ return (Property) getSource();
+ }
+
+ }
+
+ @Override
+ public void addListener(Container.PropertySetChangeListener listener) {
+ super.addListener(listener);
+ }
+
+ @Override
+ public void removeListener(Container.PropertySetChangeListener listener) {
+ super.removeListener(listener);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Property.ValueChangeNotifier#addListener(com.
+ * vaadin.data.Property.ValueChangeListener)
+ */
+ @Override
+ public void addListener(Property.ValueChangeListener listener) {
+ if (propertyValueChangeListeners == null) {
+ propertyValueChangeListeners = new LinkedList<Property.ValueChangeListener>();
+ }
+ propertyValueChangeListeners.add(listener);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Property.ValueChangeNotifier#removeListener(com
+ * .vaadin.data.Property.ValueChangeListener)
+ */
+ @Override
+ public void removeListener(Property.ValueChangeListener listener) {
+ if (propertyValueChangeListeners != null) {
+ propertyValueChangeListeners.remove(listener);
+ }
+ }
+
+ /**
+ * Sends a Property value change event to all interested listeners.
+ *
+ * @param source
+ * the IndexedContainerProperty object.
+ */
+ private void firePropertyValueChange(IndexedContainerProperty source) {
+
+ // Sends event to listeners listening all value changes
+ if (propertyValueChangeListeners != null) {
+ final Object[] l = propertyValueChangeListeners.toArray();
+ final Property.ValueChangeEvent event = new IndexedContainer.PropertyValueChangeEvent(
+ source);
+ for (int i = 0; i < l.length; i++) {
+ ((Property.ValueChangeListener) l[i]).valueChange(event);
+ }
+ }
+
+ // Sends event to single property value change listeners
+ if (singlePropertyValueChangeListeners != null) {
+ final Map<Object, List<Property.ValueChangeListener>> propertySetToListenerListMap = singlePropertyValueChangeListeners
+ .get(source.propertyId);
+ if (propertySetToListenerListMap != null) {
+ final List<Property.ValueChangeListener> listenerList = propertySetToListenerListMap
+ .get(source.itemId);
+ if (listenerList != null) {
+ final Property.ValueChangeEvent event = new IndexedContainer.PropertyValueChangeEvent(
+ source);
+ Object[] listeners = listenerList.toArray();
+ for (int i = 0; i < listeners.length; i++) {
+ ((Property.ValueChangeListener) listeners[i])
+ .valueChange(event);
+ }
+ }
+ }
+ }
+
+ }
+
+ @Override
+ public Collection<?> getListeners(Class<?> eventType) {
+ if (Property.ValueChangeEvent.class.isAssignableFrom(eventType)) {
+ if (propertyValueChangeListeners == null) {
+ return Collections.EMPTY_LIST;
+ } else {
+ return Collections
+ .unmodifiableCollection(propertyValueChangeListeners);
+ }
+ }
+ return super.getListeners(eventType);
+ }
+
+ @Override
+ protected void fireItemAdded(int position, Object itemId, Item item) {
+ if (position >= 0) {
+ fireItemSetChange(new IndexedContainer.ItemSetChangeEvent(this,
+ position));
+ }
+ }
+
+ @Override
+ protected void fireItemSetChange() {
+ fireItemSetChange(new IndexedContainer.ItemSetChangeEvent(this, -1));
+ }
+
+ /**
+ * Adds new single Property change listener.
+ *
+ * @param propertyId
+ * the ID of the Property to add.
+ * @param itemId
+ * the ID of the Item .
+ * @param listener
+ * the listener to be added.
+ */
+ private void addSinglePropertyChangeListener(Object propertyId,
+ Object itemId, Property.ValueChangeListener listener) {
+ if (listener != null) {
+ if (singlePropertyValueChangeListeners == null) {
+ singlePropertyValueChangeListeners = new Hashtable<Object, Map<Object, List<Property.ValueChangeListener>>>();
+ }
+ Map<Object, List<Property.ValueChangeListener>> propertySetToListenerListMap = singlePropertyValueChangeListeners
+ .get(propertyId);
+ if (propertySetToListenerListMap == null) {
+ propertySetToListenerListMap = new Hashtable<Object, List<Property.ValueChangeListener>>();
+ singlePropertyValueChangeListeners.put(propertyId,
+ propertySetToListenerListMap);
+ }
+ List<Property.ValueChangeListener> listenerList = propertySetToListenerListMap
+ .get(itemId);
+ if (listenerList == null) {
+ listenerList = new LinkedList<Property.ValueChangeListener>();
+ propertySetToListenerListMap.put(itemId, listenerList);
+ }
+ listenerList.add(listener);
+ }
+ }
+
+ /**
+ * Removes a previously registered single Property change listener.
+ *
+ * @param propertyId
+ * the ID of the Property to remove.
+ * @param itemId
+ * the ID of the Item.
+ * @param listener
+ * the listener to be removed.
+ */
+ private void removeSinglePropertyChangeListener(Object propertyId,
+ Object itemId, Property.ValueChangeListener listener) {
+ if (listener != null && singlePropertyValueChangeListeners != null) {
+ final Map<Object, List<Property.ValueChangeListener>> propertySetToListenerListMap = singlePropertyValueChangeListeners
+ .get(propertyId);
+ if (propertySetToListenerListMap != null) {
+ final List<Property.ValueChangeListener> listenerList = propertySetToListenerListMap
+ .get(itemId);
+ if (listenerList != null) {
+ listenerList.remove(listener);
+ if (listenerList.isEmpty()) {
+ propertySetToListenerListMap.remove(itemId);
+ }
+ }
+ if (propertySetToListenerListMap.isEmpty()) {
+ singlePropertyValueChangeListeners.remove(propertyId);
+ }
+ }
+ if (singlePropertyValueChangeListeners.isEmpty()) {
+ singlePropertyValueChangeListeners = null;
+ }
+ }
+ }
+
+ /* Internal Item and Property implementations */
+
+ /*
+ * A class implementing the com.vaadin.data.Item interface to be contained
+ * in the list.
+ *
+ * @author Vaadin Ltd.
+ *
+ * @version @VERSION@
+ *
+ * @since 3.0
+ */
+ class IndexedContainerItem implements Item {
+
+ /**
+ * Item ID in the host container for this Item.
+ */
+ private final Object itemId;
+
+ /**
+ * Constructs a new ListItem instance and connects it to a host
+ * container.
+ *
+ * @param itemId
+ * the Item ID of the new Item.
+ */
+ private IndexedContainerItem(Object itemId) {
+
+ // Gets the item contents from the host
+ if (itemId == null) {
+ throw new NullPointerException();
+ }
+ this.itemId = itemId;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Item#getItemProperty(java.lang.Object)
+ */
+ @Override
+ public Property<?> getItemProperty(Object id) {
+ return new IndexedContainerProperty(itemId, id);
+ }
+
+ @Override
+ public Collection<?> getItemPropertyIds() {
+ return Collections.unmodifiableCollection(propertyIds);
+ }
+
+ /**
+ * Gets the <code>String</code> representation of the contents of the
+ * Item. The format of the string is a space separated catenation of the
+ * <code>String</code> representations of the values of the Properties
+ * contained by the Item.
+ *
+ * @return <code>String</code> representation of the Item contents
+ */
+ @Override
+ public String toString() {
+ String retValue = "";
+
+ for (final Iterator<?> i = propertyIds.iterator(); i.hasNext();) {
+ final Object propertyId = i.next();
+ retValue += getItemProperty(propertyId).getValue();
+ if (i.hasNext()) {
+ retValue += " ";
+ }
+ }
+
+ return retValue;
+ }
+
+ /**
+ * Calculates a integer hash-code for the Item that's unique inside the
+ * list. Two Items inside the same list have always different
+ * hash-codes, though Items in different lists may have identical
+ * hash-codes.
+ *
+ * @return A locally unique hash-code as integer
+ */
+ @Override
+ public int hashCode() {
+ return itemId.hashCode();
+ }
+
+ /**
+ * Tests if the given object is the same as the this object. Two Items
+ * got from a list container with the same ID are equal.
+ *
+ * @param obj
+ * an object to compare with this object
+ * @return <code>true</code> if the given object is the same as this
+ * object, <code>false</code> if not
+ */
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == null
+ || !obj.getClass().equals(IndexedContainerItem.class)) {
+ return false;
+ }
+ final IndexedContainerItem li = (IndexedContainerItem) obj;
+ return getHost() == li.getHost() && itemId.equals(li.itemId);
+ }
+
+ private IndexedContainer getHost() {
+ return IndexedContainer.this;
+ }
+
+ /**
+ * IndexedContainerItem does not support adding new properties. Add
+ * properties at container level. See
+ * {@link IndexedContainer#addContainerProperty(Object, Class, Object)}
+ *
+ * @see com.vaadin.data.Item#addProperty(Object, Property)
+ */
+ @Override
+ public boolean addItemProperty(Object id, Property property)
+ throws UnsupportedOperationException {
+ throw new UnsupportedOperationException("Indexed container item "
+ + "does not support adding new properties");
+ }
+
+ /**
+ * Indexed container does not support removing properties. Remove
+ * properties at container level. See
+ * {@link IndexedContainer#removeContainerProperty(Object)}
+ *
+ * @see com.vaadin.data.Item#removeProperty(Object)
+ */
+ @Override
+ public boolean removeItemProperty(Object id)
+ throws UnsupportedOperationException {
+ throw new UnsupportedOperationException(
+ "Indexed container item does not support property removal");
+ }
+
+ }
+
+ /**
+ * A class implementing the {@link Property} interface to be contained in
+ * the {@link IndexedContainerItem} contained in the
+ * {@link IndexedContainer}.
+ *
+ * @author Vaadin Ltd.
+ *
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+ private class IndexedContainerProperty implements Property<Object>,
+ Property.ValueChangeNotifier {
+
+ /**
+ * ID of the Item, where this property resides.
+ */
+ private final Object itemId;
+
+ /**
+ * Id of the Property.
+ */
+ private final Object propertyId;
+
+ /**
+ * Constructs a new {@link IndexedContainerProperty} object.
+ *
+ * @param itemId
+ * the ID of the Item to connect the new Property to.
+ * @param propertyId
+ * the Property ID of the new Property.
+ * @param host
+ * the list that contains the Item to contain the new
+ * Property.
+ */
+ private IndexedContainerProperty(Object itemId, Object propertyId) {
+ if (itemId == null || propertyId == null) {
+ // Null ids are not accepted
+ throw new NullPointerException(
+ "Container item or property ids can not be null");
+ }
+ this.propertyId = propertyId;
+ this.itemId = itemId;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Property#getType()
+ */
+ @Override
+ public Class<?> getType() {
+ return types.get(propertyId);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Property#getValue()
+ */
+ @Override
+ public Object getValue() {
+ return items.get(itemId).get(propertyId);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Property#isReadOnly()
+ */
+ @Override
+ public boolean isReadOnly() {
+ return readOnlyProperties.contains(this);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Property#setReadOnly(boolean)
+ */
+ @Override
+ public void setReadOnly(boolean newStatus) {
+ if (newStatus) {
+ readOnlyProperties.add(this);
+ } else {
+ readOnlyProperties.remove(this);
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Property#setValue(java.lang.Object)
+ */
+ @Override
+ public void setValue(Object newValue) throws Property.ReadOnlyException {
+ // Gets the Property set
+ final Map<Object, Object> propertySet = items.get(itemId);
+
+ // Support null values on all types
+ if (newValue == null) {
+ propertySet.remove(propertyId);
+ } else if (getType().isAssignableFrom(newValue.getClass())) {
+ propertySet.put(propertyId, newValue);
+ } else {
+ throw new IllegalArgumentException(
+ "Value is of invalid type, got "
+ + newValue.getClass().getName() + " but "
+ + getType().getName() + " was expected");
+ }
+
+ // update the container filtering if this property is being filtered
+ if (isPropertyFiltered(propertyId)) {
+ filterAll();
+ }
+
+ firePropertyValueChange(this);
+ }
+
+ /**
+ * Returns the value of the Property in human readable textual format.
+ * The return value should be assignable to the <code>setValue</code>
+ * method if the Property is not in read-only mode.
+ *
+ * @return <code>String</code> representation of the value stored in the
+ * Property
+ * @deprecated use {@link #getValue()} instead and possibly toString on
+ * that
+ */
+ @Deprecated
+ @Override
+ public String toString() {
+ throw new UnsupportedOperationException(
+ "Use Property.getValue() instead of IndexedContainerProperty.toString()");
+ }
+
+ /**
+ * Calculates a integer hash-code for the Property that's unique inside
+ * the Item containing the Property. Two different Properties inside the
+ * same Item contained in the same list always have different
+ * hash-codes, though Properties in different Items may have identical
+ * hash-codes.
+ *
+ * @return A locally unique hash-code as integer
+ */
+ @Override
+ public int hashCode() {
+ return itemId.hashCode() ^ propertyId.hashCode();
+ }
+
+ /**
+ * Tests if the given object is the same as the this object. Two
+ * Properties got from an Item with the same ID are equal.
+ *
+ * @param obj
+ * an object to compare with this object
+ * @return <code>true</code> if the given object is the same as this
+ * object, <code>false</code> if not
+ */
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == null
+ || !obj.getClass().equals(IndexedContainerProperty.class)) {
+ return false;
+ }
+ final IndexedContainerProperty lp = (IndexedContainerProperty) obj;
+ return lp.getHost() == getHost()
+ && lp.propertyId.equals(propertyId)
+ && lp.itemId.equals(itemId);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Property.ValueChangeNotifier#addListener(
+ * com.vaadin.data.Property.ValueChangeListener)
+ */
+ @Override
+ public void addListener(Property.ValueChangeListener listener) {
+ addSinglePropertyChangeListener(propertyId, itemId, listener);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Property.ValueChangeNotifier#removeListener
+ * (com.vaadin.data.Property.ValueChangeListener)
+ */
+ @Override
+ public void removeListener(Property.ValueChangeListener listener) {
+ removeSinglePropertyChangeListener(propertyId, itemId, listener);
+ }
+
+ private IndexedContainer getHost() {
+ return IndexedContainer.this;
+ }
+
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container.Sortable#sort(java.lang.Object[],
+ * boolean[])
+ */
+ @Override
+ public void sort(Object[] propertyId, boolean[] ascending) {
+ sortContainer(propertyId, ascending);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container.Sortable#getSortableContainerPropertyIds
+ * ()
+ */
+ @Override
+ public Collection<?> getSortableContainerPropertyIds() {
+ return getSortablePropertyIds();
+ }
+
+ @Override
+ public ItemSorter getItemSorter() {
+ return super.getItemSorter();
+ }
+
+ @Override
+ public void setItemSorter(ItemSorter itemSorter) {
+ super.setItemSorter(itemSorter);
+ }
+
+ /**
+ * Supports cloning of the IndexedContainer cleanly.
+ *
+ * @throws CloneNotSupportedException
+ * if an object cannot be cloned. .
+ *
+ * @deprecated cloning support might be removed from IndexedContainer in the
+ * future
+ */
+ @Deprecated
+ @Override
+ public Object clone() throws CloneNotSupportedException {
+
+ // Creates the clone
+ final IndexedContainer nc = new IndexedContainer();
+
+ // Clone the shallow properties
+ nc.setAllItemIds(getAllItemIds() != null ? (ListSet<Object>) ((ListSet<Object>) getAllItemIds())
+ .clone() : null);
+ nc.setItemSetChangeListeners(getItemSetChangeListeners() != null ? new LinkedList<Container.ItemSetChangeListener>(
+ getItemSetChangeListeners()) : null);
+ nc.propertyIds = propertyIds != null ? (ArrayList<Object>) propertyIds
+ .clone() : null;
+ nc.setPropertySetChangeListeners(getPropertySetChangeListeners() != null ? new LinkedList<Container.PropertySetChangeListener>(
+ getPropertySetChangeListeners()) : null);
+ nc.propertyValueChangeListeners = propertyValueChangeListeners != null ? (LinkedList<Property.ValueChangeListener>) propertyValueChangeListeners
+ .clone() : null;
+ nc.readOnlyProperties = readOnlyProperties != null ? (HashSet<Property<?>>) readOnlyProperties
+ .clone() : null;
+ nc.singlePropertyValueChangeListeners = singlePropertyValueChangeListeners != null ? (Hashtable<Object, Map<Object, List<Property.ValueChangeListener>>>) singlePropertyValueChangeListeners
+ .clone() : null;
+
+ nc.types = types != null ? (Hashtable<Object, Class<?>>) types.clone()
+ : null;
+
+ nc.setFilters((HashSet<Filter>) ((HashSet<Filter>) getFilters())
+ .clone());
+
+ nc.setFilteredItemIds(getFilteredItemIds() == null ? null
+ : (ListSet<Object>) ((ListSet<Object>) getFilteredItemIds())
+ .clone());
+
+ // Clone property-values
+ if (items == null) {
+ nc.items = null;
+ } else {
+ nc.items = new Hashtable<Object, Map<Object, Object>>();
+ for (final Iterator<?> i = items.keySet().iterator(); i.hasNext();) {
+ final Object id = i.next();
+ final Hashtable<Object, Object> it = (Hashtable<Object, Object>) items
+ .get(id);
+ nc.items.put(id, (Map<Object, Object>) it.clone());
+ }
+ }
+
+ return nc;
+ }
+
+ @Override
+ public void addContainerFilter(Object propertyId, String filterString,
+ boolean ignoreCase, boolean onlyMatchPrefix) {
+ try {
+ addFilter(new SimpleStringFilter(propertyId, filterString,
+ ignoreCase, onlyMatchPrefix));
+ } catch (UnsupportedFilterException e) {
+ // the filter instance created here is always valid for in-memory
+ // containers
+ }
+ }
+
+ @Override
+ public void removeAllContainerFilters() {
+ removeAllFilters();
+ }
+
+ @Override
+ public void removeContainerFilters(Object propertyId) {
+ removeFilters(propertyId);
+ }
+
+ @Override
+ public void addContainerFilter(Filter filter)
+ throws UnsupportedFilterException {
+ addFilter(filter);
+ }
+
+ @Override
+ public void removeContainerFilter(Filter filter) {
+ removeFilter(filter);
+ }
+
+}
diff --git a/server/src/com/vaadin/data/util/ItemSorter.java b/server/src/com/vaadin/data/util/ItemSorter.java
new file mode 100644
index 0000000000..4399dbe292
--- /dev/null
+++ b/server/src/com/vaadin/data/util/ItemSorter.java
@@ -0,0 +1,57 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util;
+
+import java.io.Serializable;
+import java.util.Comparator;
+
+import com.vaadin.data.Container;
+import com.vaadin.data.Container.Sortable;
+
+/**
+ * An item comparator which is compatible with the {@link Sortable} interface.
+ * The <code>ItemSorter</code> interface can be used in <code>Sortable</code>
+ * implementations to provide a custom sorting method.
+ */
+public interface ItemSorter extends Comparator<Object>, Cloneable, Serializable {
+
+ /**
+ * Sets the parameters for an upcoming sort operation. The parameters
+ * determine what container to sort and how the <code>ItemSorter</code>
+ * sorts the container.
+ *
+ * @param container
+ * The container that will be sorted. The container must contain
+ * the propertyIds given in the <code>propertyId</code>
+ * parameter.
+ * @param propertyId
+ * The property ids used for sorting. The property ids must exist
+ * in the container and should only be used if they are also
+ * sortable, i.e include in the collection returned by
+ * <code>container.getSortableContainerPropertyIds()</code>. See
+ * {@link Sortable#sort(Object[], boolean[])} for more
+ * information.
+ * @param ascending
+ * Sorting order flags for each property id. See
+ * {@link Sortable#sort(Object[], boolean[])} for more
+ * information.
+ */
+ void setSortProperties(Container.Sortable container, Object[] propertyId,
+ boolean[] ascending);
+
+ /**
+ * Compares its two arguments for order. Returns a negative integer, zero,
+ * or a positive integer as the first argument is less than, equal to, or
+ * greater than the second.
+ * <p>
+ * The parameters for the <code>ItemSorter</code> <code>compare()</code>
+ * method must always be item ids which exist in the container set using
+ * {@link #setSortProperties(Sortable, Object[], boolean[])}.
+ *
+ * @see Comparator#compare(Object, Object)
+ */
+ @Override
+ int compare(Object itemId1, Object itemId2);
+
+}
diff --git a/server/src/com/vaadin/data/util/ListSet.java b/server/src/com/vaadin/data/util/ListSet.java
new file mode 100644
index 0000000000..b71cc46898
--- /dev/null
+++ b/server/src/com/vaadin/data/util/ListSet.java
@@ -0,0 +1,264 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+
+/**
+ * ListSet is an internal Vaadin class which implements a combination of a List
+ * and a Set. The main purpose of this class is to provide a list with a fast
+ * {@link #contains(Object)} method. Each inserted object must by unique (as
+ * specified by {@link #equals(Object)}). The {@link #set(int, Object)} method
+ * allows duplicates because of the way {@link Collections#sort(java.util.List)}
+ * works.
+ *
+ * This class is subject to change and should not be used outside Vaadin core.
+ */
+public class ListSet<E> extends ArrayList<E> {
+ private HashSet<E> itemSet = null;
+
+ /**
+ * Contains a map from an element to the number of duplicates it has. Used
+ * to temporarily allow duplicates in the list.
+ */
+ private HashMap<E, Integer> duplicates = new HashMap<E, Integer>();
+
+ public ListSet() {
+ super();
+ itemSet = new HashSet<E>();
+ }
+
+ public ListSet(Collection<? extends E> c) {
+ super(c);
+ itemSet = new HashSet<E>(c.size());
+ itemSet.addAll(c);
+ }
+
+ public ListSet(int initialCapacity) {
+ super(initialCapacity);
+ itemSet = new HashSet<E>(initialCapacity);
+ }
+
+ // Delegate contains operations to the set
+ @Override
+ public boolean contains(Object o) {
+ return itemSet.contains(o);
+ }
+
+ @Override
+ public boolean containsAll(Collection<?> c) {
+ return itemSet.containsAll(c);
+ }
+
+ // Methods for updating the set when the list is updated.
+ @Override
+ public boolean add(E e) {
+ if (contains(e)) {
+ // Duplicates are not allowed
+ return false;
+ }
+
+ if (super.add(e)) {
+ itemSet.add(e);
+ return true;
+ } else {
+ return false;
+ }
+ };
+
+ /**
+ * Works as java.util.ArrayList#add(int, java.lang.Object) but returns
+ * immediately if the element is already in the ListSet.
+ */
+ @Override
+ public void add(int index, E element) {
+ if (contains(element)) {
+ // Duplicates are not allowed
+ return;
+ }
+
+ super.add(index, element);
+ itemSet.add(element);
+ }
+
+ @Override
+ public boolean addAll(Collection<? extends E> c) {
+ boolean modified = false;
+ Iterator<? extends E> i = c.iterator();
+ while (i.hasNext()) {
+ E e = i.next();
+ if (contains(e)) {
+ continue;
+ }
+
+ if (add(e)) {
+ itemSet.add(e);
+ modified = true;
+ }
+ }
+ return modified;
+ }
+
+ @Override
+ public boolean addAll(int index, Collection<? extends E> c) {
+ ensureCapacity(size() + c.size());
+
+ boolean modified = false;
+ Iterator<? extends E> i = c.iterator();
+ while (i.hasNext()) {
+ E e = i.next();
+ if (contains(e)) {
+ continue;
+ }
+
+ add(index++, e);
+ itemSet.add(e);
+ modified = true;
+ }
+
+ return modified;
+ }
+
+ @Override
+ public void clear() {
+ super.clear();
+ itemSet.clear();
+ }
+
+ @Override
+ public int indexOf(Object o) {
+ if (!contains(o)) {
+ return -1;
+ }
+
+ return super.indexOf(o);
+ }
+
+ @Override
+ public int lastIndexOf(Object o) {
+ if (!contains(o)) {
+ return -1;
+ }
+
+ return super.lastIndexOf(o);
+ }
+
+ @Override
+ public E remove(int index) {
+ E e = super.remove(index);
+
+ if (e != null) {
+ itemSet.remove(e);
+ }
+
+ return e;
+ }
+
+ @Override
+ public boolean remove(Object o) {
+ if (super.remove(o)) {
+ itemSet.remove(o);
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ protected void removeRange(int fromIndex, int toIndex) {
+ HashSet<E> toRemove = new HashSet<E>();
+ for (int idx = fromIndex; idx < toIndex; idx++) {
+ toRemove.add(get(idx));
+ }
+ super.removeRange(fromIndex, toIndex);
+ itemSet.removeAll(toRemove);
+ }
+
+ @Override
+ public E set(int index, E element) {
+ if (contains(element)) {
+ // Element already exist in the list
+ if (get(index) == element) {
+ // At the same position, nothing to be done
+ return element;
+ } else {
+ // Adding at another position. We assume this is a sort
+ // operation and temporarily allow it.
+
+ // We could just remove (null) the old element and keep the list
+ // unique. This would require finding the index of the old
+ // element (indexOf(element)) which is not a fast operation in a
+ // list. So we instead allow duplicates temporarily.
+ addDuplicate(element);
+ }
+ }
+
+ E old = super.set(index, element);
+ removeFromSet(old);
+ itemSet.add(element);
+
+ return old;
+ }
+
+ /**
+ * Removes "e" from the set if it no longer exists in the list.
+ *
+ * @param e
+ */
+ private void removeFromSet(E e) {
+ Integer dupl = duplicates.get(e);
+ if (dupl != null) {
+ // A duplicate was present so we only decrement the duplicate count
+ // and continue
+ if (dupl == 1) {
+ // This is what always should happen. A sort sets the items one
+ // by one, temporarily breaking the uniqueness requirement.
+ duplicates.remove(e);
+ } else {
+ duplicates.put(e, dupl - 1);
+ }
+ } else {
+ // The "old" value is no longer in the list.
+ itemSet.remove(e);
+ }
+
+ }
+
+ /**
+ * Marks the "element" can be found more than once from the list. Allowed in
+ * {@link #set(int, Object)} to make sorting work.
+ *
+ * @param element
+ */
+ private void addDuplicate(E element) {
+ Integer nr = duplicates.get(element);
+ if (nr == null) {
+ nr = 1;
+ } else {
+ nr++;
+ }
+
+ /*
+ * Store the number of duplicates of this element so we know later on if
+ * we should remove an element from the set or if it was a duplicate (in
+ * removeFromSet)
+ */
+ duplicates.put(element, nr);
+
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public Object clone() {
+ ListSet<E> v = (ListSet<E>) super.clone();
+ v.itemSet = new HashSet<E>(itemSet);
+ return v;
+ }
+
+}
diff --git a/server/src/com/vaadin/data/util/MethodProperty.java b/server/src/com/vaadin/data/util/MethodProperty.java
new file mode 100644
index 0000000000..0c64d90481
--- /dev/null
+++ b/server/src/com/vaadin/data/util/MethodProperty.java
@@ -0,0 +1,784 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.data.util;
+
+import java.io.IOException;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import com.vaadin.data.Property;
+import com.vaadin.util.SerializerHelper;
+
+/**
+ * <p>
+ * Proxy class for creating Properties from pairs of getter and setter methods
+ * of a Bean property. An instance of this class can be thought as having been
+ * attached to a field of an object. Accessing the object through the Property
+ * interface directly manipulates the underlying field.
+ * </p>
+ *
+ * <p>
+ * It's assumed that the return value returned by the getter method is
+ * assignable to the type of the property, and the setter method parameter is
+ * assignable to that value.
+ * </p>
+ *
+ * <p>
+ * A valid getter method must always be available, but instance of this class
+ * can be constructed with a <code>null</code> setter method in which case the
+ * resulting MethodProperty is read-only.
+ * </p>
+ *
+ * <p>
+ * MethodProperty implements Property.ValueChangeNotifier, but does not
+ * automatically know whether or not the getter method will actually return a
+ * new value - value change listeners are always notified when setValue is
+ * called, without verifying what the getter returns.
+ * </p>
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+@SuppressWarnings("serial")
+public class MethodProperty<T> extends AbstractProperty<T> {
+
+ /**
+ * The object that includes the property the MethodProperty is bound to.
+ */
+ private transient Object instance;
+
+ /**
+ * Argument arrays for the getter and setter methods.
+ */
+ private transient Object[] setArgs, getArgs;
+
+ /**
+ * The getter and setter methods.
+ */
+ private transient Method setMethod, getMethod;
+
+ /**
+ * Index of the new value in the argument list for the setter method. If the
+ * setter method requires several parameters, this index tells which one is
+ * the actual value to change.
+ */
+ private int setArgumentIndex;
+
+ /**
+ * Type of the property.
+ */
+ private transient Class<? extends T> type;
+
+ /* Special serialization to handle method references */
+ private void writeObject(java.io.ObjectOutputStream out) throws IOException {
+ out.defaultWriteObject();
+ SerializerHelper.writeClass(out, type);
+ out.writeObject(instance);
+ out.writeObject(setArgs);
+ out.writeObject(getArgs);
+ if (setMethod != null) {
+ out.writeObject(setMethod.getName());
+ SerializerHelper
+ .writeClassArray(out, setMethod.getParameterTypes());
+ } else {
+ out.writeObject(null);
+ out.writeObject(null);
+ }
+ if (getMethod != null) {
+ out.writeObject(getMethod.getName());
+ SerializerHelper
+ .writeClassArray(out, getMethod.getParameterTypes());
+ } else {
+ out.writeObject(null);
+ out.writeObject(null);
+ }
+ };
+
+ /* Special serialization to handle method references */
+ private void readObject(java.io.ObjectInputStream in) throws IOException,
+ ClassNotFoundException {
+ in.defaultReadObject();
+ try {
+ @SuppressWarnings("unchecked")
+ // business assumption; type parameters not checked at runtime
+ Class<T> class1 = (Class<T>) SerializerHelper.readClass(in);
+ type = class1;
+ instance = in.readObject();
+ setArgs = (Object[]) in.readObject();
+ getArgs = (Object[]) in.readObject();
+ String name = (String) in.readObject();
+ Class<?>[] paramTypes = SerializerHelper.readClassArray(in);
+ if (name != null) {
+ setMethod = instance.getClass().getMethod(name, paramTypes);
+ } else {
+ setMethod = null;
+ }
+
+ name = (String) in.readObject();
+ paramTypes = SerializerHelper.readClassArray(in);
+ if (name != null) {
+ getMethod = instance.getClass().getMethod(name, paramTypes);
+ } else {
+ getMethod = null;
+ }
+ } catch (SecurityException e) {
+ getLogger().log(Level.SEVERE, "Internal deserialization error", e);
+ } catch (NoSuchMethodException e) {
+ getLogger().log(Level.SEVERE, "Internal deserialization error", e);
+ }
+ };
+
+ /**
+ * <p>
+ * Creates a new instance of <code>MethodProperty</code> from a named bean
+ * property. This constructor takes an object and the name of a bean
+ * property and initializes itself with the accessor methods for the
+ * property.
+ * </p>
+ * <p>
+ * The getter method of a <code>MethodProperty</code> instantiated with this
+ * constructor will be called with no arguments, and the setter method with
+ * only the new value as the sole argument.
+ * </p>
+ *
+ * <p>
+ * If the setter method is unavailable, the resulting
+ * <code>MethodProperty</code> will be read-only, otherwise it will be
+ * read-write.
+ * </p>
+ *
+ * <p>
+ * Method names are constructed from the bean property by adding
+ * get/is/are/set prefix and capitalising the first character in the name of
+ * the given bean property.
+ * </p>
+ *
+ * @param instance
+ * the object that includes the property.
+ * @param beanPropertyName
+ * the name of the property to bind to.
+ */
+ @SuppressWarnings("unchecked")
+ public MethodProperty(Object instance, String beanPropertyName) {
+
+ final Class<?> beanClass = instance.getClass();
+
+ // Assure that the first letter is upper cased (it is a common
+ // mistake to write firstName, not FirstName).
+ if (Character.isLowerCase(beanPropertyName.charAt(0))) {
+ final char[] buf = beanPropertyName.toCharArray();
+ buf[0] = Character.toUpperCase(buf[0]);
+ beanPropertyName = new String(buf);
+ }
+
+ // Find the get method
+ getMethod = null;
+ try {
+ getMethod = initGetterMethod(beanPropertyName, beanClass);
+ } catch (final java.lang.NoSuchMethodException ignored) {
+ throw new MethodException(this, "Bean property " + beanPropertyName
+ + " can not be found");
+ }
+
+ // In case the get method is found, resolve the type
+ Class<?> returnType = getMethod.getReturnType();
+
+ // Finds the set method
+ setMethod = null;
+ try {
+ setMethod = beanClass.getMethod("set" + beanPropertyName,
+ new Class[] { returnType });
+ } catch (final java.lang.NoSuchMethodException skipped) {
+ }
+
+ // Gets the return type from get method
+ if (returnType.isPrimitive()) {
+ type = (Class<T>) convertPrimitiveType(returnType);
+ if (type.isPrimitive()) {
+ throw new MethodException(this, "Bean property "
+ + beanPropertyName
+ + " getter return type must not be void");
+ }
+ } else {
+ type = (Class<T>) returnType;
+ }
+
+ setArguments(new Object[] {}, new Object[] { null }, 0);
+ this.instance = instance;
+ }
+
+ /**
+ * <p>
+ * Creates a new instance of <code>MethodProperty</code> from named getter
+ * and setter methods. The getter method of a <code>MethodProperty</code>
+ * instantiated with this constructor will be called with no arguments, and
+ * the setter method with only the new value as the sole argument.
+ * </p>
+ *
+ * <p>
+ * If the setter method is <code>null</code>, the resulting
+ * <code>MethodProperty</code> will be read-only, otherwise it will be
+ * read-write.
+ * </p>
+ *
+ * @param type
+ * the type of the property.
+ * @param instance
+ * the object that includes the property.
+ * @param getMethodName
+ * the name of the getter method.
+ * @param setMethodName
+ * the name of the setter method.
+ *
+ */
+ public MethodProperty(Class<? extends T> type, Object instance,
+ String getMethodName, String setMethodName) {
+ this(type, instance, getMethodName, setMethodName, new Object[] {},
+ new Object[] { null }, 0);
+ }
+
+ /**
+ * <p>
+ * Creates a new instance of <code>MethodProperty</code> with the getter and
+ * setter methods. The getter method of a <code>MethodProperty</code>
+ * instantiated with this constructor will be called with no arguments, and
+ * the setter method with only the new value as the sole argument.
+ * </p>
+ *
+ * <p>
+ * If the setter method is <code>null</code>, the resulting
+ * <code>MethodProperty</code> will be read-only, otherwise it will be
+ * read-write.
+ * </p>
+ *
+ * @param type
+ * the type of the property.
+ * @param instance
+ * the object that includes the property.
+ * @param getMethod
+ * the getter method.
+ * @param setMethod
+ * the setter method.
+ */
+ public MethodProperty(Class<? extends T> type, Object instance,
+ Method getMethod, Method setMethod) {
+ this(type, instance, getMethod, setMethod, new Object[] {},
+ new Object[] { null }, 0);
+ }
+
+ /**
+ * <p>
+ * Creates a new instance of <code>MethodProperty</code> from named getter
+ * and setter methods and argument lists. The getter method of a
+ * <code>MethodProperty</code> instantiated with this constructor will be
+ * called with the getArgs as arguments. The setArgs will be used as the
+ * arguments for the setter method, though the argument indexed by the
+ * setArgumentIndex will be replaced with the argument passed to the
+ * {@link #setValue(Object newValue)} method.
+ * </p>
+ *
+ * <p>
+ * For example, if the <code>setArgs</code> contains <code>A</code>,
+ * <code>B</code> and <code>C</code>, and <code>setArgumentIndex =
+ * 1</code>, the call <code>methodProperty.setValue(X)</code> would result
+ * in the setter method to be called with the parameter set of
+ * <code>{A, X, C}</code>
+ * </p>
+ *
+ * @param type
+ * the type of the property.
+ * @param instance
+ * the object that includes the property.
+ * @param getMethodName
+ * the name of the getter method.
+ * @param setMethodName
+ * the name of the setter method.
+ * @param getArgs
+ * the fixed argument list to be passed to the getter method.
+ * @param setArgs
+ * the fixed argument list to be passed to the setter method.
+ * @param setArgumentIndex
+ * the index of the argument in <code>setArgs</code> to be
+ * replaced with <code>newValue</code> when
+ * {@link #setValue(Object newValue)} is called.
+ */
+ @SuppressWarnings("unchecked")
+ public MethodProperty(Class<? extends T> type, Object instance,
+ String getMethodName, String setMethodName, Object[] getArgs,
+ Object[] setArgs, int setArgumentIndex) {
+
+ // Check the setargs and setargs index
+ if (setMethodName != null && setArgs == null) {
+ throw new IndexOutOfBoundsException("The setArgs can not be null");
+ }
+ if (setMethodName != null
+ && (setArgumentIndex < 0 || setArgumentIndex >= setArgs.length)) {
+ throw new IndexOutOfBoundsException(
+ "The setArgumentIndex must be >= 0 and < setArgs.length");
+ }
+
+ // Set type
+ this.type = type;
+
+ // Find set and get -methods
+ final Method[] m = instance.getClass().getMethods();
+
+ // Finds get method
+ boolean found = false;
+ for (int i = 0; i < m.length; i++) {
+
+ // Tests the name of the get Method
+ if (!m[i].getName().equals(getMethodName)) {
+
+ // name does not match, try next method
+ continue;
+ }
+
+ // Tests return type
+ if (!type.equals(m[i].getReturnType())) {
+ continue;
+ }
+
+ // Tests the parameter types
+ final Class<?>[] c = m[i].getParameterTypes();
+ if (c.length != getArgs.length) {
+
+ // not the right amount of parameters, try next method
+ continue;
+ }
+ int j = 0;
+ while (j < c.length) {
+ if (getArgs[j] != null
+ && !c[j].isAssignableFrom(getArgs[j].getClass())) {
+
+ // parameter type does not match, try next method
+ break;
+ }
+ j++;
+ }
+ if (j == c.length) {
+
+ // all paramteters matched
+ if (found == true) {
+ throw new MethodException(this,
+ "Could not uniquely identify " + getMethodName
+ + "-method");
+ } else {
+ found = true;
+ getMethod = m[i];
+ }
+ }
+ }
+ if (found != true) {
+ throw new MethodException(this, "Could not find " + getMethodName
+ + "-method");
+ }
+
+ // Finds set method
+ if (setMethodName != null) {
+
+ // Finds setMethod
+ found = false;
+ for (int i = 0; i < m.length; i++) {
+
+ // Checks name
+ if (!m[i].getName().equals(setMethodName)) {
+
+ // name does not match, try next method
+ continue;
+ }
+
+ // Checks parameter compatibility
+ final Class<?>[] c = m[i].getParameterTypes();
+ if (c.length != setArgs.length) {
+
+ // not the right amount of parameters, try next method
+ continue;
+ }
+ int j = 0;
+ while (j < c.length) {
+ if (setArgs[j] != null
+ && !c[j].isAssignableFrom(setArgs[j].getClass())) {
+
+ // parameter type does not match, try next method
+ break;
+ } else if (j == setArgumentIndex && !c[j].equals(type)) {
+
+ // Property type is not the same as setArg type
+ break;
+ }
+ j++;
+ }
+ if (j == c.length) {
+
+ // all parameters match
+ if (found == true) {
+ throw new MethodException(this,
+ "Could not identify unique " + setMethodName
+ + "-method");
+ } else {
+ found = true;
+ setMethod = m[i];
+ }
+ }
+ }
+ if (found != true) {
+ throw new MethodException(this, "Could not identify "
+ + setMethodName + "-method");
+ }
+ }
+
+ // Gets the return type from get method
+ this.type = (Class<T>) convertPrimitiveType(type);
+
+ setArguments(getArgs, setArgs, setArgumentIndex);
+ this.instance = instance;
+ }
+
+ /**
+ * <p>
+ * Creates a new instance of <code>MethodProperty</code> from the getter and
+ * setter methods, and argument lists.
+ * </p>
+ * <p>
+ * This constructor behaves exactly like
+ * {@link #MethodProperty(Class type, Object instance, String getMethodName, String setMethodName, Object [] getArgs, Object [] setArgs, int setArgumentIndex)}
+ * except that instead of names of the getter and setter methods this
+ * constructor is given the actual methods themselves.
+ * </p>
+ *
+ * @param type
+ * the type of the property.
+ * @param instance
+ * the object that includes the property.
+ * @param getMethod
+ * the getter method.
+ * @param setMethod
+ * the setter method.
+ * @param getArgs
+ * the fixed argument list to be passed to the getter method.
+ * @param setArgs
+ * the fixed argument list to be passed to the setter method.
+ * @param setArgumentIndex
+ * the index of the argument in <code>setArgs</code> to be
+ * replaced with <code>newValue</code> when
+ * {@link #setValue(Object newValue)} is called.
+ */
+ @SuppressWarnings("unchecked")
+ // cannot use "Class<? extends T>" because of automatic primitive type
+ // conversions
+ public MethodProperty(Class<?> type, Object instance, Method getMethod,
+ Method setMethod, Object[] getArgs, Object[] setArgs,
+ int setArgumentIndex) {
+
+ if (getMethod == null) {
+ throw new MethodException(this,
+ "Property GET-method cannot not be null: " + type);
+ }
+
+ if (setMethod != null) {
+ if (setArgs == null) {
+ throw new IndexOutOfBoundsException(
+ "The setArgs can not be null");
+ }
+ if (setArgumentIndex < 0 || setArgumentIndex >= setArgs.length) {
+ throw new IndexOutOfBoundsException(
+ "The setArgumentIndex must be >= 0 and < setArgs.length");
+ }
+ }
+
+ // Gets the return type from get method
+ Class<? extends T> convertedType = (Class<? extends T>) convertPrimitiveType(type);
+
+ this.getMethod = getMethod;
+ this.setMethod = setMethod;
+ setArguments(getArgs, setArgs, setArgumentIndex);
+ this.instance = instance;
+ this.type = convertedType;
+ }
+
+ /**
+ * Find a getter method for a property (getXyz(), isXyz() or areXyz()).
+ *
+ * @param propertyName
+ * name of the property
+ * @param beanClass
+ * class in which to look for the getter methods
+ * @return Method
+ * @throws NoSuchMethodException
+ * if no getter found
+ */
+ static Method initGetterMethod(String propertyName, final Class<?> beanClass)
+ throws NoSuchMethodException {
+ propertyName = propertyName.substring(0, 1).toUpperCase()
+ + propertyName.substring(1);
+
+ Method getMethod = null;
+ try {
+ getMethod = beanClass.getMethod("get" + propertyName,
+ new Class[] {});
+ } catch (final java.lang.NoSuchMethodException ignored) {
+ try {
+ getMethod = beanClass.getMethod("is" + propertyName,
+ new Class[] {});
+ } catch (final java.lang.NoSuchMethodException ignoredAsWell) {
+ getMethod = beanClass.getMethod("are" + propertyName,
+ new Class[] {});
+ }
+ }
+ return getMethod;
+ }
+
+ static Class<?> convertPrimitiveType(Class<?> type) {
+ // Gets the return type from get method
+ if (type.isPrimitive()) {
+ if (type.equals(Boolean.TYPE)) {
+ type = Boolean.class;
+ } else if (type.equals(Integer.TYPE)) {
+ type = Integer.class;
+ } else if (type.equals(Float.TYPE)) {
+ type = Float.class;
+ } else if (type.equals(Double.TYPE)) {
+ type = Double.class;
+ } else if (type.equals(Byte.TYPE)) {
+ type = Byte.class;
+ } else if (type.equals(Character.TYPE)) {
+ type = Character.class;
+ } else if (type.equals(Short.TYPE)) {
+ type = Short.class;
+ } else if (type.equals(Long.TYPE)) {
+ type = Long.class;
+ }
+ }
+ return type;
+ }
+
+ /**
+ * Returns the type of the Property. The methods <code>getValue</code> and
+ * <code>setValue</code> must be compatible with this type: one must be able
+ * to safely cast the value returned from <code>getValue</code> to the given
+ * type and pass any variable assignable to this type as an argument to
+ * <code>setValue</code>.
+ *
+ * @return type of the Property
+ */
+ @Override
+ public final Class<? extends T> getType() {
+ return type;
+ }
+
+ /**
+ * Tests if the object is in read-only mode. In read-only mode calls to
+ * <code>setValue</code> will throw <code>ReadOnlyException</code> and will
+ * not modify the value of the Property.
+ *
+ * @return <code>true</code> if the object is in read-only mode,
+ * <code>false</code> if it's not
+ */
+ @Override
+ public boolean isReadOnly() {
+ return super.isReadOnly() || (setMethod == null);
+ }
+
+ /**
+ * Gets the value stored in the Property. The value is resolved by calling
+ * the specified getter method with the argument specified at instantiation.
+ *
+ * @return the value of the Property
+ */
+ @Override
+ public T getValue() {
+ try {
+ return (T) getMethod.invoke(instance, getArgs);
+ } catch (final Throwable e) {
+ throw new MethodException(this, e);
+ }
+ }
+
+ /**
+ * <p>
+ * Sets the setter method and getter method argument lists.
+ * </p>
+ *
+ * @param getArgs
+ * the fixed argument list to be passed to the getter method.
+ * @param setArgs
+ * the fixed argument list to be passed to the setter method.
+ * @param setArgumentIndex
+ * the index of the argument in <code>setArgs</code> to be
+ * replaced with <code>newValue</code> when
+ * {@link #setValue(Object newValue)} is called.
+ */
+ public void setArguments(Object[] getArgs, Object[] setArgs,
+ int setArgumentIndex) {
+ this.getArgs = new Object[getArgs.length];
+ for (int i = 0; i < getArgs.length; i++) {
+ this.getArgs[i] = getArgs[i];
+ }
+ this.setArgs = new Object[setArgs.length];
+ for (int i = 0; i < setArgs.length; i++) {
+ this.setArgs[i] = setArgs[i];
+ }
+ this.setArgumentIndex = setArgumentIndex;
+ }
+
+ /**
+ * Sets the value of the property.
+ *
+ * Note that since Vaadin 7, no conversions are performed and the value must
+ * be of the correct type.
+ *
+ * @param newValue
+ * the New value of the property.
+ * @throws <code>Property.ReadOnlyException</code> if the object is in
+ * read-only mode.
+ * @see #invokeSetMethod(Object)
+ */
+ @Override
+ @SuppressWarnings("unchecked")
+ public void setValue(Object newValue) throws Property.ReadOnlyException {
+
+ // Checks the mode
+ if (isReadOnly()) {
+ throw new Property.ReadOnlyException();
+ }
+
+ // Checks the type of the value
+ if (newValue != null && !type.isAssignableFrom(newValue.getClass())) {
+ throw new IllegalArgumentException(
+ "Invalid value type for ObjectProperty.");
+ }
+
+ invokeSetMethod((T) newValue);
+ fireValueChange();
+ }
+
+ /**
+ * Internal method to actually call the setter method of the wrapped
+ * property.
+ *
+ * @param value
+ */
+ protected void invokeSetMethod(T value) {
+
+ try {
+ // Construct a temporary argument array only if needed
+ if (setArgs.length == 1) {
+ setMethod.invoke(instance, new Object[] { value });
+ } else {
+
+ // Sets the value to argument array
+ final Object[] args = new Object[setArgs.length];
+ for (int i = 0; i < setArgs.length; i++) {
+ args[i] = (i == setArgumentIndex) ? value : setArgs[i];
+ }
+ setMethod.invoke(instance, args);
+ }
+ } catch (final InvocationTargetException e) {
+ final Throwable targetException = e.getTargetException();
+ throw new MethodException(this, targetException);
+ } catch (final Exception e) {
+ throw new MethodException(this, e);
+ }
+ }
+
+ /**
+ * <code>Exception</code> object that signals that there were problems
+ * calling or finding the specified getter or setter methods of the
+ * property.
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+ @SuppressWarnings("rawtypes")
+ // Exceptions cannot be parameterized, ever.
+ public static class MethodException extends RuntimeException {
+
+ /**
+ * The method property from which the exception originates from
+ */
+ private final Property property;
+
+ /**
+ * Cause of the method exception
+ */
+ private Throwable cause;
+
+ /**
+ * Constructs a new <code>MethodException</code> with the specified
+ * detail message.
+ *
+ * @param property
+ * the property.
+ * @param msg
+ * the detail message.
+ */
+ public MethodException(Property property, String msg) {
+ super(msg);
+ this.property = property;
+ }
+
+ /**
+ * Constructs a new <code>MethodException</code> from another exception.
+ *
+ * @param property
+ * the property.
+ * @param cause
+ * the cause of the exception.
+ */
+ public MethodException(Property property, Throwable cause) {
+ this.property = property;
+ this.cause = cause;
+ }
+
+ /**
+ * @see java.lang.Throwable#getCause()
+ */
+ @Override
+ public Throwable getCause() {
+ return cause;
+ }
+
+ /**
+ * Gets the method property this exception originates from.
+ *
+ * @return MethodProperty or null if not a valid MethodProperty
+ */
+ public MethodProperty getMethodProperty() {
+ return (property instanceof MethodProperty) ? (MethodProperty) property
+ : null;
+ }
+
+ /**
+ * Gets the method property this exception originates from.
+ *
+ * @return Property from which the exception originates
+ */
+ public Property getProperty() {
+ return property;
+ }
+ }
+
+ /**
+ * Sends a value change event to all registered listeners.
+ *
+ * Public for backwards compatibility, visibility may be reduced in future
+ * versions.
+ */
+ @Override
+ public void fireValueChange() {
+ super.fireValueChange();
+ }
+
+ private static final Logger getLogger() {
+ return Logger.getLogger(MethodProperty.class.getName());
+ }
+}
diff --git a/server/src/com/vaadin/data/util/MethodPropertyDescriptor.java b/server/src/com/vaadin/data/util/MethodPropertyDescriptor.java
new file mode 100644
index 0000000000..a2a76ec6cf
--- /dev/null
+++ b/server/src/com/vaadin/data/util/MethodPropertyDescriptor.java
@@ -0,0 +1,134 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util;
+
+import java.io.IOException;
+import java.lang.reflect.Method;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import com.vaadin.data.Property;
+import com.vaadin.util.SerializerHelper;
+
+/**
+ * Property descriptor that is able to create simple {@link MethodProperty}
+ * instances for a bean, using given accessors.
+ *
+ * @param <BT>
+ * bean type
+ *
+ * @since 6.6
+ */
+public class MethodPropertyDescriptor<BT> implements
+ VaadinPropertyDescriptor<BT> {
+
+ private final String name;
+ private Class<?> propertyType;
+ private transient Method readMethod;
+ private transient Method writeMethod;
+
+ /**
+ * Creates a property descriptor that can create MethodProperty instances to
+ * access the underlying bean property.
+ *
+ * @param name
+ * of the property
+ * @param propertyType
+ * type (class) of the property
+ * @param readMethod
+ * getter {@link Method} for the property
+ * @param writeMethod
+ * setter {@link Method} for the property or null if read-only
+ * property
+ */
+ public MethodPropertyDescriptor(String name, Class<?> propertyType,
+ Method readMethod, Method writeMethod) {
+ this.name = name;
+ this.propertyType = propertyType;
+ this.readMethod = readMethod;
+ this.writeMethod = writeMethod;
+ }
+
+ /* Special serialization to handle method references */
+ private void writeObject(java.io.ObjectOutputStream out) throws IOException {
+ out.defaultWriteObject();
+ SerializerHelper.writeClass(out, propertyType);
+
+ if (writeMethod != null) {
+ out.writeObject(writeMethod.getName());
+ SerializerHelper.writeClass(out, writeMethod.getDeclaringClass());
+ SerializerHelper.writeClassArray(out,
+ writeMethod.getParameterTypes());
+ } else {
+ out.writeObject(null);
+ out.writeObject(null);
+ out.writeObject(null);
+ }
+
+ if (readMethod != null) {
+ out.writeObject(readMethod.getName());
+ SerializerHelper.writeClass(out, readMethod.getDeclaringClass());
+ SerializerHelper.writeClassArray(out,
+ readMethod.getParameterTypes());
+ } else {
+ out.writeObject(null);
+ out.writeObject(null);
+ out.writeObject(null);
+ }
+ }
+
+ /* Special serialization to handle method references */
+ private void readObject(java.io.ObjectInputStream in) throws IOException,
+ ClassNotFoundException {
+ in.defaultReadObject();
+ try {
+ @SuppressWarnings("unchecked")
+ // business assumption; type parameters not checked at runtime
+ Class<BT> class1 = (Class<BT>) SerializerHelper.readClass(in);
+ propertyType = class1;
+
+ String name = (String) in.readObject();
+ Class<?> writeMethodClass = SerializerHelper.readClass(in);
+ Class<?>[] paramTypes = SerializerHelper.readClassArray(in);
+ if (name != null) {
+ writeMethod = writeMethodClass.getMethod(name, paramTypes);
+ } else {
+ writeMethod = null;
+ }
+
+ name = (String) in.readObject();
+ Class<?> readMethodClass = SerializerHelper.readClass(in);
+ paramTypes = SerializerHelper.readClassArray(in);
+ if (name != null) {
+ readMethod = readMethodClass.getMethod(name, paramTypes);
+ } else {
+ readMethod = null;
+ }
+ } catch (SecurityException e) {
+ getLogger().log(Level.SEVERE, "Internal deserialization error", e);
+ } catch (NoSuchMethodException e) {
+ getLogger().log(Level.SEVERE, "Internal deserialization error", e);
+ }
+ };
+
+ @Override
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public Class<?> getPropertyType() {
+ return propertyType;
+ }
+
+ @Override
+ public Property<?> createProperty(Object bean) {
+ return new MethodProperty<Object>(propertyType, bean, readMethod,
+ writeMethod);
+ }
+
+ private static final Logger getLogger() {
+ return Logger.getLogger(MethodPropertyDescriptor.class.getName());
+ }
+} \ No newline at end of file
diff --git a/server/src/com/vaadin/data/util/NestedMethodProperty.java b/server/src/com/vaadin/data/util/NestedMethodProperty.java
new file mode 100644
index 0000000000..9bff38456d
--- /dev/null
+++ b/server/src/com/vaadin/data/util/NestedMethodProperty.java
@@ -0,0 +1,257 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util;
+
+import java.io.IOException;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import com.vaadin.data.Property;
+import com.vaadin.data.util.MethodProperty.MethodException;
+
+/**
+ * Nested accessor based property for a bean.
+ *
+ * The property is specified in the dotted notation, e.g. "address.street", and
+ * can contain multiple levels of nesting.
+ *
+ * When accessing the property value, all intermediate getters must return
+ * non-null values.
+ *
+ * @see MethodProperty
+ *
+ * @since 6.6
+ */
+public class NestedMethodProperty<T> extends AbstractProperty<T> {
+
+ // needed for de-serialization
+ private String propertyName;
+
+ // chain of getter methods
+ private transient List<Method> getMethods;
+ /**
+ * The setter method.
+ */
+ private transient Method setMethod;
+
+ /**
+ * Bean instance used as a starting point for accessing the property value.
+ */
+ private Object instance;
+
+ private Class<? extends T> type;
+
+ /* Special serialization to handle method references */
+ private void writeObject(java.io.ObjectOutputStream out) throws IOException {
+ out.defaultWriteObject();
+ // getMethods and setMethod are reconstructed on read based on
+ // propertyName
+ }
+
+ /* Special serialization to handle method references */
+ private void readObject(java.io.ObjectInputStream in) throws IOException,
+ ClassNotFoundException {
+ in.defaultReadObject();
+
+ initialize(instance.getClass(), propertyName);
+ }
+
+ /**
+ * Constructs a nested method property for a given object instance. The
+ * property name is a dot separated string pointing to a nested property,
+ * e.g. "manager.address.street".
+ *
+ * @param instance
+ * top-level bean to which the property applies
+ * @param propertyName
+ * dot separated nested property name
+ * @throws IllegalArgumentException
+ * if the property name is invalid
+ */
+ public NestedMethodProperty(Object instance, String propertyName) {
+ this.instance = instance;
+ initialize(instance.getClass(), propertyName);
+ }
+
+ /**
+ * For internal use to deduce property type etc. without a bean instance.
+ * Calling {@link #setValue(Object)} or {@link #getValue()} on properties
+ * constructed this way is not supported.
+ *
+ * @param instanceClass
+ * class of the top-level bean
+ * @param propertyName
+ */
+ NestedMethodProperty(Class<?> instanceClass, String propertyName) {
+ instance = null;
+ initialize(instanceClass, propertyName);
+ }
+
+ /**
+ * Initializes most of the internal fields based on the top-level bean
+ * instance and property name (dot-separated string).
+ *
+ * @param beanClass
+ * class of the top-level bean to which the property applies
+ * @param propertyName
+ * dot separated nested property name
+ * @throws IllegalArgumentException
+ * if the property name is invalid
+ */
+ private void initialize(Class<?> beanClass, String propertyName)
+ throws IllegalArgumentException {
+
+ List<Method> getMethods = new ArrayList<Method>();
+
+ String lastSimplePropertyName = propertyName;
+ Class<?> lastClass = beanClass;
+
+ // first top-level property, then go deeper in a loop
+ Class<?> propertyClass = beanClass;
+ String[] simplePropertyNames = propertyName.split("\\.");
+ if (propertyName.endsWith(".") || 0 == simplePropertyNames.length) {
+ throw new IllegalArgumentException("Invalid property name '"
+ + propertyName + "'");
+ }
+ for (int i = 0; i < simplePropertyNames.length; i++) {
+ String simplePropertyName = simplePropertyNames[i].trim();
+ if (simplePropertyName.length() > 0) {
+ lastSimplePropertyName = simplePropertyName;
+ lastClass = propertyClass;
+ try {
+ Method getter = MethodProperty.initGetterMethod(
+ simplePropertyName, propertyClass);
+ propertyClass = getter.getReturnType();
+ getMethods.add(getter);
+ } catch (final java.lang.NoSuchMethodException e) {
+ throw new IllegalArgumentException("Bean property '"
+ + simplePropertyName + "' not found", e);
+ }
+ } else {
+ throw new IllegalArgumentException(
+ "Empty or invalid bean property identifier in '"
+ + propertyName + "'");
+ }
+ }
+
+ // In case the get method is found, resolve the type
+ Method lastGetMethod = getMethods.get(getMethods.size() - 1);
+ Class<?> type = lastGetMethod.getReturnType();
+
+ // Finds the set method
+ Method setMethod = null;
+ try {
+ // Assure that the first letter is upper cased (it is a common
+ // mistake to write firstName, not FirstName).
+ if (Character.isLowerCase(lastSimplePropertyName.charAt(0))) {
+ final char[] buf = lastSimplePropertyName.toCharArray();
+ buf[0] = Character.toUpperCase(buf[0]);
+ lastSimplePropertyName = new String(buf);
+ }
+
+ setMethod = lastClass.getMethod("set" + lastSimplePropertyName,
+ new Class[] { type });
+ } catch (final NoSuchMethodException skipped) {
+ }
+
+ this.type = (Class<? extends T>) MethodProperty
+ .convertPrimitiveType(type);
+ this.propertyName = propertyName;
+ this.getMethods = getMethods;
+ this.setMethod = setMethod;
+ }
+
+ @Override
+ public Class<? extends T> getType() {
+ return type;
+ }
+
+ @Override
+ public boolean isReadOnly() {
+ return super.isReadOnly() || (null == setMethod);
+ }
+
+ /**
+ * Gets the value stored in the Property. The value is resolved by calling
+ * the specified getter method with the argument specified at instantiation.
+ *
+ * @return the value of the Property
+ */
+ @Override
+ public T getValue() {
+ try {
+ Object object = instance;
+ for (Method m : getMethods) {
+ object = m.invoke(object);
+ }
+ return (T) object;
+ } catch (final Throwable e) {
+ throw new MethodException(this, e);
+ }
+ }
+
+ /**
+ * Sets the value of the property. The new value must be assignable to the
+ * type of this property.
+ *
+ * @param newValue
+ * the New value of the property.
+ * @throws <code>Property.ReadOnlyException</code> if the object is in
+ * read-only mode.
+ * @see #invokeSetMethod(Object)
+ */
+ @Override
+ public void setValue(Object newValue) throws ReadOnlyException {
+ // Checks the mode
+ if (isReadOnly()) {
+ throw new Property.ReadOnlyException();
+ }
+
+ // Checks the type of the value
+ if (newValue != null && !type.isAssignableFrom(newValue.getClass())) {
+ throw new IllegalArgumentException(
+ "Invalid value type for NestedMethodProperty.");
+ }
+
+ invokeSetMethod((T) newValue);
+ fireValueChange();
+ }
+
+ /**
+ * Internal method to actually call the setter method of the wrapped
+ * property.
+ *
+ * @param value
+ */
+ protected void invokeSetMethod(T value) {
+ try {
+ Object object = instance;
+ for (int i = 0; i < getMethods.size() - 1; i++) {
+ object = getMethods.get(i).invoke(object);
+ }
+ setMethod.invoke(object, new Object[] { value });
+ } catch (final InvocationTargetException e) {
+ throw new MethodException(this, e.getTargetException());
+ } catch (final Exception e) {
+ throw new MethodException(this, e);
+ }
+ }
+
+ /**
+ * Returns an unmodifiable list of getter methods to call in sequence to get
+ * the property value.
+ *
+ * This API may change in future versions.
+ *
+ * @return unmodifiable list of getter methods corresponding to each segment
+ * of the property name
+ */
+ protected List<Method> getGetMethods() {
+ return Collections.unmodifiableList(getMethods);
+ }
+
+}
diff --git a/server/src/com/vaadin/data/util/NestedPropertyDescriptor.java b/server/src/com/vaadin/data/util/NestedPropertyDescriptor.java
new file mode 100644
index 0000000000..b67b425d1d
--- /dev/null
+++ b/server/src/com/vaadin/data/util/NestedPropertyDescriptor.java
@@ -0,0 +1,60 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util;
+
+import com.vaadin.data.Property;
+
+/**
+ * Property descriptor that is able to create nested property instances for a
+ * bean.
+ *
+ * The property is specified in the dotted notation, e.g. "address.street", and
+ * can contain multiple levels of nesting.
+ *
+ * @param <BT>
+ * bean type
+ *
+ * @since 6.6
+ */
+public class NestedPropertyDescriptor<BT> implements
+ VaadinPropertyDescriptor<BT> {
+
+ private final String name;
+ private final Class<?> propertyType;
+
+ /**
+ * Creates a property descriptor that can create MethodProperty instances to
+ * access the underlying bean property.
+ *
+ * @param name
+ * of the property in a dotted path format, e.g. "address.street"
+ * @param beanType
+ * type (class) of the top-level bean
+ * @throws IllegalArgumentException
+ * if the property name is invalid
+ */
+ public NestedPropertyDescriptor(String name, Class<BT> beanType)
+ throws IllegalArgumentException {
+ this.name = name;
+ NestedMethodProperty<?> property = new NestedMethodProperty<Object>(
+ beanType, name);
+ this.propertyType = property.getType();
+ }
+
+ @Override
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public Class<?> getPropertyType() {
+ return propertyType;
+ }
+
+ @Override
+ public Property<?> createProperty(BT bean) {
+ return new NestedMethodProperty<Object>(bean, name);
+ }
+
+}
diff --git a/server/src/com/vaadin/data/util/ObjectProperty.java b/server/src/com/vaadin/data/util/ObjectProperty.java
new file mode 100644
index 0000000000..cb85b44c2a
--- /dev/null
+++ b/server/src/com/vaadin/data/util/ObjectProperty.java
@@ -0,0 +1,141 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.data.util;
+
+import com.vaadin.data.Property;
+
+/**
+ * A simple data object containing one typed value. This class is a
+ * straightforward implementation of the the {@link com.vaadin.data.Property}
+ * interface.
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+@SuppressWarnings("serial")
+public class ObjectProperty<T> extends AbstractProperty<T> {
+
+ /**
+ * The value contained by the Property.
+ */
+ private T value;
+
+ /**
+ * Data type of the Property's value.
+ */
+ private final Class<T> type;
+
+ /**
+ * Creates a new instance of ObjectProperty with the given value. The type
+ * of the property is automatically initialized to be the type of the given
+ * value.
+ *
+ * @param value
+ * the Initial value of the Property.
+ */
+ @SuppressWarnings("unchecked")
+ // the cast is safe, because an object of type T has class Class<T>
+ public ObjectProperty(T value) {
+ this(value, (Class<T>) value.getClass());
+ }
+
+ /**
+ * Creates a new instance of ObjectProperty with the given value and type.
+ *
+ * Since Vaadin 7, only values of the correct type are accepted, and no
+ * automatic conversions are performed.
+ *
+ * @param value
+ * the Initial value of the Property.
+ * @param type
+ * the type of the value. The value must be assignable to given
+ * type.
+ */
+ public ObjectProperty(T value, Class<T> type) {
+
+ // Set the values
+ this.type = type;
+ setValue(value);
+ }
+
+ /**
+ * Creates a new instance of ObjectProperty with the given value, type and
+ * read-only mode status.
+ *
+ * Since Vaadin 7, only the correct type of values is accepted, see
+ * {@link #ObjectProperty(Object, Class)}.
+ *
+ * @param value
+ * the Initial value of the property.
+ * @param type
+ * the type of the value. <code>value</code> must be assignable
+ * to this type.
+ * @param readOnly
+ * Sets the read-only mode.
+ */
+ public ObjectProperty(T value, Class<T> type, boolean readOnly) {
+ this(value, type);
+ setReadOnly(readOnly);
+ }
+
+ /**
+ * Returns the type of the ObjectProperty. The methods <code>getValue</code>
+ * and <code>setValue</code> must be compatible with this type: one must be
+ * able to safely cast the value returned from <code>getValue</code> to the
+ * given type and pass any variable assignable to this type as an argument
+ * to <code>setValue</code>.
+ *
+ * @return type of the Property
+ */
+ @Override
+ public final Class<T> getType() {
+ return type;
+ }
+
+ /**
+ * Gets the value stored in the Property.
+ *
+ * @return the value stored in the Property
+ */
+ @Override
+ public T getValue() {
+ return value;
+ }
+
+ /**
+ * Sets the value of the property.
+ *
+ * Note that since Vaadin 7, no conversions are performed and the value must
+ * be of the correct type.
+ *
+ * @param newValue
+ * the New value of the property.
+ * @throws <code>Property.ReadOnlyException</code> if the object is in
+ * read-only mode
+ */
+ @Override
+ @SuppressWarnings("unchecked")
+ public void setValue(Object newValue) throws Property.ReadOnlyException {
+
+ // Checks the mode
+ if (isReadOnly()) {
+ throw new Property.ReadOnlyException();
+ }
+
+ // Checks the type of the value
+ if (newValue != null && !type.isAssignableFrom(newValue.getClass())) {
+ throw new IllegalArgumentException("Invalid value type "
+ + newValue.getClass().getName()
+ + " for ObjectProperty of type " + type.getName() + ".");
+ }
+
+ // the cast is safe after an isAssignableFrom check
+ this.value = (T) newValue;
+
+ fireValueChange();
+ }
+}
diff --git a/server/src/com/vaadin/data/util/PropertyFormatter.java b/server/src/com/vaadin/data/util/PropertyFormatter.java
new file mode 100644
index 0000000000..3d65726309
--- /dev/null
+++ b/server/src/com/vaadin/data/util/PropertyFormatter.java
@@ -0,0 +1,245 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util;
+
+import com.vaadin.data.Property;
+import com.vaadin.data.util.converter.Converter;
+
+/**
+ * Formatting proxy for a {@link Property}.
+ *
+ * <p>
+ * This class can be used to implement formatting for any type of Property
+ * datasources. The idea is to connect this as proxy between UI component and
+ * the original datasource.
+ * </p>
+ *
+ * <p>
+ * For example <code>
+ * <pre>textfield.setPropertyDataSource(new PropertyFormatter(property) {
+ public String format(Object value) {
+ return ((Double) value).toString() + "000000000";
+ }
+
+ public Object parse(String formattedValue) throws Exception {
+ return Double.parseDouble(formattedValue);
+ }
+
+ });</pre></code> adds formatter for Double-typed property that extends
+ * standard "1.0" notation with more zeroes.
+ * </p>
+ *
+ * @param T
+ * type of the underlying property (a PropertyFormatter is always a
+ * Property&lt;String&gt;)
+ *
+ * @deprecated Since 7.0 replaced by {@link Converter}
+ * @author Vaadin Ltd.
+ * @since 5.3.0
+ */
+@SuppressWarnings("serial")
+@Deprecated
+public abstract class PropertyFormatter<T> extends AbstractProperty<String>
+ implements Property.Viewer, Property.ValueChangeListener,
+ Property.ReadOnlyStatusChangeListener {
+
+ /** Datasource that stores the actual value. */
+ Property<T> dataSource;
+
+ /**
+ * Construct a new {@code PropertyFormatter} that is not connected to any
+ * data source. Call {@link #setPropertyDataSource(Property)} later on to
+ * attach it to a property.
+ *
+ */
+ protected PropertyFormatter() {
+ }
+
+ /**
+ * Construct a new formatter that is connected to given data source. Calls
+ * {@link #format(Object)} which can be a problem if the formatter has not
+ * yet been initialized.
+ *
+ * @param propertyDataSource
+ * to connect this property to.
+ */
+ public PropertyFormatter(Property<T> propertyDataSource) {
+
+ setPropertyDataSource(propertyDataSource);
+ }
+
+ /**
+ * Gets the current data source of the formatter, if any.
+ *
+ * @return the current data source as a Property, or <code>null</code> if
+ * none defined.
+ */
+ @Override
+ public Property<T> getPropertyDataSource() {
+ return dataSource;
+ }
+
+ /**
+ * Sets the specified Property as the data source for the formatter.
+ *
+ *
+ * <p>
+ * Remember that new data sources getValue() must return objects that are
+ * compatible with parse() and format() methods.
+ * </p>
+ *
+ * @param newDataSource
+ * the new data source Property.
+ */
+ @Override
+ public void setPropertyDataSource(Property newDataSource) {
+
+ boolean readOnly = false;
+ String prevValue = null;
+
+ if (dataSource != null) {
+ if (dataSource instanceof Property.ValueChangeNotifier) {
+ ((Property.ValueChangeNotifier) dataSource)
+ .removeListener(this);
+ }
+ if (dataSource instanceof Property.ReadOnlyStatusChangeListener) {
+ ((Property.ReadOnlyStatusChangeNotifier) dataSource)
+ .removeListener(this);
+ }
+ readOnly = isReadOnly();
+ prevValue = getValue();
+ }
+
+ dataSource = newDataSource;
+
+ if (dataSource != null) {
+ if (dataSource instanceof Property.ValueChangeNotifier) {
+ ((Property.ValueChangeNotifier) dataSource).addListener(this);
+ }
+ if (dataSource instanceof Property.ReadOnlyStatusChangeListener) {
+ ((Property.ReadOnlyStatusChangeNotifier) dataSource)
+ .addListener(this);
+ }
+ }
+
+ if (isReadOnly() != readOnly) {
+ fireReadOnlyStatusChange();
+ }
+ String newVal = getValue();
+ if ((prevValue == null && newVal != null)
+ || (prevValue != null && !prevValue.equals(newVal))) {
+ fireValueChange();
+ }
+ }
+
+ /* Documented in the interface */
+ @Override
+ public Class<String> getType() {
+ return String.class;
+ }
+
+ /**
+ * Get the formatted value.
+ *
+ * @return If the datasource returns null, this is null. Otherwise this is
+ * String given by format().
+ */
+ @Override
+ public String getValue() {
+ T value = dataSource == null ? null : dataSource.getValue();
+ if (value == null) {
+ return null;
+ }
+ return format(value);
+ }
+
+ /** Reflects the read-only status of the datasource. */
+ @Override
+ public boolean isReadOnly() {
+ return dataSource == null ? false : dataSource.isReadOnly();
+ }
+
+ /**
+ * This method must be implemented to format the values received from
+ * DataSource.
+ *
+ * @param value
+ * Value object got from the datasource. This is guaranteed to be
+ * non-null and of the type compatible with getType() of the
+ * datasource.
+ * @return
+ */
+ abstract public String format(T value);
+
+ /**
+ * Parse string and convert it to format compatible with datasource.
+ *
+ * The method is required to assure that parse(format(x)) equals x.
+ *
+ * @param formattedValue
+ * This is guaranteed to be non-null string.
+ * @return Non-null value compatible with datasource.
+ * @throws Exception
+ * Any type of exception can be thrown to indicate that the
+ * conversion was not succesful.
+ */
+ abstract public T parse(String formattedValue) throws Exception;
+
+ /**
+ * Sets the Property's read-only mode to the specified status.
+ *
+ * @param newStatus
+ * the new read-only status of the Property.
+ */
+ @Override
+ public void setReadOnly(boolean newStatus) {
+ if (dataSource != null) {
+ dataSource.setReadOnly(newStatus);
+ }
+ }
+
+ @Override
+ public void setValue(Object newValue) throws ReadOnlyException {
+ if (dataSource == null) {
+ return;
+ }
+ if (newValue == null) {
+ if (dataSource.getValue() != null) {
+ dataSource.setValue(null);
+ fireValueChange();
+ }
+ } else {
+ try {
+ dataSource.setValue(parse(newValue.toString()));
+ if (!newValue.equals(getValue())) {
+ fireValueChange();
+ }
+ } catch (Exception e) {
+ throw new IllegalArgumentException("Could not parse value", e);
+ }
+ }
+ }
+
+ /**
+ * Listens for changes in the datasource.
+ *
+ * This should not be called directly.
+ */
+ @Override
+ public void valueChange(com.vaadin.data.Property.ValueChangeEvent event) {
+ fireValueChange();
+ }
+
+ /**
+ * Listens for changes in the datasource.
+ *
+ * This should not be called directly.
+ */
+ @Override
+ public void readOnlyStatusChange(
+ com.vaadin.data.Property.ReadOnlyStatusChangeEvent event) {
+ fireReadOnlyStatusChange();
+ }
+
+}
diff --git a/server/src/com/vaadin/data/util/PropertysetItem.java b/server/src/com/vaadin/data/util/PropertysetItem.java
new file mode 100644
index 0000000000..22f2da75b2
--- /dev/null
+++ b/server/src/com/vaadin/data/util/PropertysetItem.java
@@ -0,0 +1,340 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.data.util;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.EventObject;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedList;
+
+import com.vaadin.data.Item;
+import com.vaadin.data.Property;
+
+/**
+ * Class for handling a set of identified Properties. The elements contained in
+ * a </code>MapItem</code> can be referenced using locally unique identifiers.
+ * The class supports listeners who are interested in changes to the Property
+ * set managed by the class.
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+@SuppressWarnings("serial")
+public class PropertysetItem implements Item, Item.PropertySetChangeNotifier,
+ Cloneable {
+
+ /* Private representation of the item */
+
+ /**
+ * Mapping from property id to property.
+ */
+ private HashMap<Object, Property<?>> map = new HashMap<Object, Property<?>>();
+
+ /**
+ * List of all property ids to maintain the order.
+ */
+ private LinkedList<Object> list = new LinkedList<Object>();
+
+ /**
+ * List of property set modification listeners.
+ */
+ private LinkedList<Item.PropertySetChangeListener> propertySetChangeListeners = null;
+
+ /* Item methods */
+
+ /**
+ * Gets the Property corresponding to the given Property ID stored in the
+ * Item. If the Item does not contain the Property, <code>null</code> is
+ * returned.
+ *
+ * @param id
+ * the identifier of the Property to get.
+ * @return the Property with the given ID or <code>null</code>
+ */
+ @Override
+ public Property<?> getItemProperty(Object id) {
+ return map.get(id);
+ }
+
+ /**
+ * Gets the collection of IDs of all Properties stored in the Item.
+ *
+ * @return unmodifiable collection containing IDs of the Properties stored
+ * the Item
+ */
+ @Override
+ public Collection<?> getItemPropertyIds() {
+ return Collections.unmodifiableCollection(list);
+ }
+
+ /* Item.Managed methods */
+
+ /**
+ * Removes the Property identified by ID from the Item. This functionality
+ * is optional. If the method is not implemented, the method always returns
+ * <code>false</code>.
+ *
+ * @param id
+ * the ID of the Property to be removed.
+ * @return <code>true</code> if the operation succeeded <code>false</code>
+ * if not
+ */
+ @Override
+ public boolean removeItemProperty(Object id) {
+
+ // Cant remove missing properties
+ if (map.remove(id) == null) {
+ return false;
+ }
+ list.remove(id);
+
+ // Send change events
+ fireItemPropertySetChange();
+
+ return true;
+ }
+
+ /**
+ * Tries to add a new Property into the Item.
+ *
+ * @param id
+ * the ID of the new Property.
+ * @param property
+ * the Property to be added and associated with the id.
+ * @return <code>true</code> if the operation succeeded, <code>false</code>
+ * if not
+ */
+ @Override
+ public boolean addItemProperty(Object id, Property property) {
+
+ // Null ids are not accepted
+ if (id == null) {
+ throw new NullPointerException("Item property id can not be null");
+ }
+
+ // Cant add a property twice
+ if (map.containsKey(id)) {
+ return false;
+ }
+
+ // Put the property to map
+ map.put(id, property);
+ list.add(id);
+
+ // Send event
+ fireItemPropertySetChange();
+
+ return true;
+ }
+
+ /**
+ * Gets the <code>String</code> representation of the contents of the Item.
+ * The format of the string is a space separated catenation of the
+ * <code>String</code> representations of the Properties contained by the
+ * Item.
+ *
+ * @return <code>String</code> representation of the Item contents
+ */
+ @Override
+ public String toString() {
+ String retValue = "";
+
+ for (final Iterator<?> i = getItemPropertyIds().iterator(); i.hasNext();) {
+ final Object propertyId = i.next();
+ retValue += getItemProperty(propertyId).getValue();
+ if (i.hasNext()) {
+ retValue += " ";
+ }
+ }
+
+ return retValue;
+ }
+
+ /* Notifiers */
+
+ /**
+ * An <code>event</code> object specifying an Item whose Property set has
+ * changed.
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+ private static class PropertySetChangeEvent extends EventObject implements
+ Item.PropertySetChangeEvent {
+
+ private PropertySetChangeEvent(Item source) {
+ super(source);
+ }
+
+ /**
+ * Gets the Item whose Property set has changed.
+ *
+ * @return source object of the event as an <code>Item</code>
+ */
+ @Override
+ public Item getItem() {
+ return (Item) getSource();
+ }
+ }
+
+ /**
+ * Registers a new property set change listener for this Item.
+ *
+ * @param listener
+ * the new Listener to be registered.
+ */
+ @Override
+ public void addListener(Item.PropertySetChangeListener listener) {
+ if (propertySetChangeListeners == null) {
+ propertySetChangeListeners = new LinkedList<PropertySetChangeListener>();
+ }
+ propertySetChangeListeners.add(listener);
+ }
+
+ /**
+ * Removes a previously registered property set change listener.
+ *
+ * @param listener
+ * the Listener to be removed.
+ */
+ @Override
+ public void removeListener(Item.PropertySetChangeListener listener) {
+ if (propertySetChangeListeners != null) {
+ propertySetChangeListeners.remove(listener);
+ }
+ }
+
+ /**
+ * Sends a Property set change event to all interested listeners.
+ */
+ private void fireItemPropertySetChange() {
+ if (propertySetChangeListeners != null) {
+ final Object[] l = propertySetChangeListeners.toArray();
+ final Item.PropertySetChangeEvent event = new PropertysetItem.PropertySetChangeEvent(
+ this);
+ for (int i = 0; i < l.length; i++) {
+ ((Item.PropertySetChangeListener) l[i])
+ .itemPropertySetChange(event);
+ }
+ }
+ }
+
+ public Collection<?> getListeners(Class<?> eventType) {
+ if (Item.PropertySetChangeEvent.class.isAssignableFrom(eventType)) {
+ if (propertySetChangeListeners == null) {
+ return Collections.EMPTY_LIST;
+ } else {
+ return Collections
+ .unmodifiableCollection(propertySetChangeListeners);
+ }
+ }
+
+ return Collections.EMPTY_LIST;
+ }
+
+ /**
+ * Creates and returns a copy of this object.
+ * <p>
+ * The method <code>clone</code> performs a shallow copy of the
+ * <code>PropertysetItem</code>.
+ * </p>
+ * <p>
+ * Note : All arrays are considered to implement the interface Cloneable.
+ * Otherwise, this method creates a new instance of the class of this object
+ * and initializes all its fields with exactly the contents of the
+ * corresponding fields of this object, as if by assignment, the contents of
+ * the fields are not themselves cloned. Thus, this method performs a
+ * "shallow copy" of this object, not a "deep copy" operation.
+ * </p>
+ *
+ * @throws CloneNotSupportedException
+ * if the object's class does not support the Cloneable
+ * interface.
+ *
+ * @see java.lang.Object#clone()
+ */
+ @Override
+ public Object clone() throws CloneNotSupportedException {
+
+ final PropertysetItem npsi = new PropertysetItem();
+
+ npsi.list = list != null ? (LinkedList<Object>) list.clone() : null;
+ npsi.propertySetChangeListeners = propertySetChangeListeners != null ? (LinkedList<PropertySetChangeListener>) propertySetChangeListeners
+ .clone() : null;
+ npsi.map = (HashMap<Object, Property<?>>) map.clone();
+
+ return npsi;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see java.lang.Object#equals(java.lang.Object)
+ */
+ @Override
+ public boolean equals(Object obj) {
+
+ if (obj == null || !(obj instanceof PropertysetItem)) {
+ return false;
+ }
+
+ final PropertysetItem other = (PropertysetItem) obj;
+
+ if (other.list != list) {
+ if (other.list == null) {
+ return false;
+ }
+ if (!other.list.equals(list)) {
+ return false;
+ }
+ }
+ if (other.map != map) {
+ if (other.map == null) {
+ return false;
+ }
+ if (!other.map.equals(map)) {
+ return false;
+ }
+ }
+ if (other.propertySetChangeListeners != propertySetChangeListeners) {
+ boolean thisEmpty = (propertySetChangeListeners == null || propertySetChangeListeners
+ .isEmpty());
+ boolean otherEmpty = (other.propertySetChangeListeners == null || other.propertySetChangeListeners
+ .isEmpty());
+ if (thisEmpty && otherEmpty) {
+ return true;
+ }
+ if (otherEmpty) {
+ return false;
+ }
+ if (!other.propertySetChangeListeners
+ .equals(propertySetChangeListeners)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see java.lang.Object#hashCode()
+ */
+ @Override
+ public int hashCode() {
+
+ return (list == null ? 0 : list.hashCode())
+ ^ (map == null ? 0 : map.hashCode())
+ ^ ((propertySetChangeListeners == null || propertySetChangeListeners
+ .isEmpty()) ? 0 : propertySetChangeListeners.hashCode());
+ }
+}
diff --git a/server/src/com/vaadin/data/util/QueryContainer.java b/server/src/com/vaadin/data/util/QueryContainer.java
new file mode 100644
index 0000000000..dc7c883a7e
--- /dev/null
+++ b/server/src/com/vaadin/data/util/QueryContainer.java
@@ -0,0 +1,675 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.data.util;
+
+import java.sql.Connection;
+import java.sql.ResultSet;
+import java.sql.ResultSetMetaData;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+
+import com.vaadin.data.Container;
+import com.vaadin.data.Item;
+import com.vaadin.data.Property;
+
+/**
+ * <p>
+ * The <code>QueryContainer</code> is the specialized form of Container which is
+ * Ordered and Indexed. This is used to represent the contents of relational
+ * database tables accessed through the JDBC Connection in the Vaadin Table.
+ * This creates Items based on the queryStatement provided to the container.
+ * </p>
+ *
+ * <p>
+ * The <code>QueryContainer</code> can be visualized as a representation of a
+ * relational database table.Each Item in the container represents the row
+ * fetched by the query.All cells in a column have same data type and the data
+ * type information is retrieved from the metadata of the resultset.
+ * </p>
+ *
+ * <p>
+ * Note : If data in the tables gets modified, Container will not get reflected
+ * with the updates, we have to explicity invoke QueryContainer.refresh method.
+ * {@link com.vaadin.data.util.QueryContainer#refresh() refresh()}
+ * </p>
+ *
+ * @see com.vaadin.data.Container
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @since 4.0
+ *
+ * @deprecated will be removed in the future, use the SQLContainer add-on
+ */
+
+@Deprecated
+@SuppressWarnings("serial")
+public class QueryContainer implements Container, Container.Ordered,
+ Container.Indexed {
+
+ // default ResultSet type
+ public static final int DEFAULT_RESULTSET_TYPE = ResultSet.TYPE_SCROLL_INSENSITIVE;
+
+ // default ResultSet concurrency
+ public static final int DEFAULT_RESULTSET_CONCURRENCY = ResultSet.CONCUR_READ_ONLY;
+
+ private int resultSetType = DEFAULT_RESULTSET_TYPE;
+
+ private int resultSetConcurrency = DEFAULT_RESULTSET_CONCURRENCY;
+
+ private final String queryStatement;
+
+ private final Connection connection;
+
+ private ResultSet result;
+
+ private Collection<String> propertyIds;
+
+ private final HashMap<String, Class<?>> propertyTypes = new HashMap<String, Class<?>>();
+
+ private int size = -1;
+
+ private Statement statement;
+
+ /**
+ * Constructs new <code>QueryContainer</code> with the specified
+ * <code>queryStatement</code>.
+ *
+ * @param queryStatement
+ * Database query
+ * @param connection
+ * Connection object
+ * @param resultSetType
+ * @param resultSetConcurrency
+ * @throws SQLException
+ * when database operation fails
+ */
+ public QueryContainer(String queryStatement, Connection connection,
+ int resultSetType, int resultSetConcurrency) throws SQLException {
+ this.queryStatement = queryStatement;
+ this.connection = connection;
+ this.resultSetType = resultSetType;
+ this.resultSetConcurrency = resultSetConcurrency;
+ init();
+ }
+
+ /**
+ * Constructs new <code>QueryContainer</code> with the specified
+ * queryStatement using the default resultset type and default resultset
+ * concurrency.
+ *
+ * @param queryStatement
+ * Database query
+ * @param connection
+ * Connection object
+ * @see QueryContainer#DEFAULT_RESULTSET_TYPE
+ * @see QueryContainer#DEFAULT_RESULTSET_CONCURRENCY
+ * @throws SQLException
+ * when database operation fails
+ */
+ public QueryContainer(String queryStatement, Connection connection)
+ throws SQLException {
+ this(queryStatement, connection, DEFAULT_RESULTSET_TYPE,
+ DEFAULT_RESULTSET_CONCURRENCY);
+ }
+
+ /**
+ * Fills the Container with the items and properties. Invoked by the
+ * constructor.
+ *
+ * @throws SQLException
+ * when parameter initialization fails.
+ * @see QueryContainer#QueryContainer(String, Connection, int, int).
+ */
+ private void init() throws SQLException {
+ refresh();
+ ResultSetMetaData metadata;
+ metadata = result.getMetaData();
+ final int count = metadata.getColumnCount();
+ final ArrayList<String> list = new ArrayList<String>(count);
+ for (int i = 1; i <= count; i++) {
+ final String columnName = metadata.getColumnName(i);
+ list.add(columnName);
+ final Property<?> p = getContainerProperty(new Integer(1),
+ columnName);
+ propertyTypes.put(columnName,
+ p == null ? Object.class : p.getType());
+ }
+ propertyIds = Collections.unmodifiableCollection(list);
+ }
+
+ /**
+ * <p>
+ * Restores items in the container. This method will update the latest data
+ * to the container.
+ * </p>
+ * Note: This method should be used to update the container with the latest
+ * items.
+ *
+ * @throws SQLException
+ * when database operation fails
+ *
+ */
+
+ public void refresh() throws SQLException {
+ close();
+ statement = connection.createStatement(resultSetType,
+ resultSetConcurrency);
+ result = statement.executeQuery(queryStatement);
+ result.last();
+ size = result.getRow();
+ }
+
+ /**
+ * Releases and nullifies the <code>statement</code>.
+ *
+ * @throws SQLException
+ * when database operation fails
+ */
+
+ public void close() throws SQLException {
+ if (statement != null) {
+ statement.close();
+ }
+ statement = null;
+ }
+
+ /**
+ * Gets the Item with the given Item ID from the Container.
+ *
+ * @param id
+ * ID of the Item to retrieve
+ * @return Item Id.
+ */
+
+ @Override
+ public Item getItem(Object id) {
+ return new Row(id);
+ }
+
+ /**
+ * Gets the collection of propertyId from the Container.
+ *
+ * @return Collection of Property ID.
+ */
+
+ @Override
+ public Collection<String> getContainerPropertyIds() {
+ return propertyIds;
+ }
+
+ /**
+ * Gets an collection of all the item IDs in the container.
+ *
+ * @return collection of Item IDs
+ */
+ @Override
+ public Collection<?> getItemIds() {
+ final Collection<Integer> c = new ArrayList<Integer>(size);
+ for (int i = 1; i <= size; i++) {
+ c.add(new Integer(i));
+ }
+ return c;
+ }
+
+ /**
+ * Gets the property identified by the given itemId and propertyId from the
+ * container. If the container does not contain the property
+ * <code>null</code> is returned.
+ *
+ * @param itemId
+ * ID of the Item which contains the Property
+ * @param propertyId
+ * ID of the Property to retrieve
+ *
+ * @return Property with the given ID if exists; <code>null</code>
+ * otherwise.
+ */
+
+ @Override
+ public synchronized Property<?> getContainerProperty(Object itemId,
+ Object propertyId) {
+ if (!(itemId instanceof Integer && propertyId instanceof String)) {
+ return null;
+ }
+ Object value;
+ try {
+ result.absolute(((Integer) itemId).intValue());
+ value = result.getObject((String) propertyId);
+ } catch (final Exception e) {
+ return null;
+ }
+
+ // Handle also null values from the database
+ return new ObjectProperty<Object>(value != null ? value
+ : new String(""));
+ }
+
+ /**
+ * Gets the data type of all properties identified by the given type ID.
+ *
+ * @param id
+ * ID identifying the Properties
+ *
+ * @return data type of the Properties
+ */
+
+ @Override
+ public Class<?> getType(Object id) {
+ return propertyTypes.get(id);
+ }
+
+ /**
+ * Gets the number of items in the container.
+ *
+ * @return the number of items in the container.
+ */
+ @Override
+ public int size() {
+ return size;
+ }
+
+ /**
+ * Tests if the list contains the specified Item.
+ *
+ * @param id
+ * ID the of Item to be tested.
+ * @return <code>true</code> if given id is in the container;
+ * <code>false</code> otherwise.
+ */
+ @Override
+ public boolean containsId(Object id) {
+ if (!(id instanceof Integer)) {
+ return false;
+ }
+ final int i = ((Integer) id).intValue();
+ if (i < 1) {
+ return false;
+ }
+ if (i > size) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Creates new Item with the given ID into the Container.
+ *
+ * @param itemId
+ * ID of the Item to be created.
+ *
+ * @return Created new Item, or <code>null</code> if it fails.
+ *
+ * @throws UnsupportedOperationException
+ * if the addItem method is not supported.
+ */
+ @Override
+ public Item addItem(Object itemId) throws UnsupportedOperationException {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Creates a new Item into the Container, and assign it an ID.
+ *
+ * @return ID of the newly created Item, or <code>null</code> if it fails.
+ * @throws UnsupportedOperationException
+ * if the addItem method is not supported.
+ */
+ @Override
+ public Object addItem() throws UnsupportedOperationException {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Removes the Item identified by ItemId from the Container.
+ *
+ * @param itemId
+ * ID of the Item to remove.
+ * @return <code>true</code> if the operation succeeded; <code>false</code>
+ * otherwise.
+ * @throws UnsupportedOperationException
+ * if the removeItem method is not supported.
+ */
+ @Override
+ public boolean removeItem(Object itemId)
+ throws UnsupportedOperationException {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Adds new Property to all Items in the Container.
+ *
+ * @param propertyId
+ * ID of the Property
+ * @param type
+ * Data type of the new Property
+ * @param defaultValue
+ * The value all created Properties are initialized to.
+ * @return <code>true</code> if the operation succeeded; <code>false</code>
+ * otherwise.
+ * @throws UnsupportedOperationException
+ * if the addContainerProperty method is not supported.
+ */
+ @Override
+ public boolean addContainerProperty(Object propertyId, Class<?> type,
+ Object defaultValue) throws UnsupportedOperationException {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Removes a Property specified by the given Property ID from the Container.
+ *
+ * @param propertyId
+ * ID of the Property to remove
+ * @return <code>true</code> if the operation succeeded; <code>false</code>
+ * otherwise.
+ * @throws UnsupportedOperationException
+ * if the removeContainerProperty method is not supported.
+ */
+ @Override
+ public boolean removeContainerProperty(Object propertyId)
+ throws UnsupportedOperationException {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Removes all Items from the Container.
+ *
+ * @return <code>true</code> if the operation succeeded; <code>false</code>
+ * otherwise.
+ * @throws UnsupportedOperationException
+ * if the removeAllItems method is not supported.
+ */
+ @Override
+ public boolean removeAllItems() throws UnsupportedOperationException {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Adds new item after the given item.
+ *
+ * @param previousItemId
+ * Id of the previous item in ordered container.
+ * @param newItemId
+ * Id of the new item to be added.
+ * @return Returns new item or <code>null</code> if the operation fails.
+ * @throws UnsupportedOperationException
+ * if the addItemAfter method is not supported.
+ */
+ @Override
+ public Item addItemAfter(Object previousItemId, Object newItemId)
+ throws UnsupportedOperationException {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Adds new item after the given item.
+ *
+ * @param previousItemId
+ * Id of the previous item in ordered container.
+ * @return Returns item id created new item or <code>null</code> if the
+ * operation fails.
+ * @throws UnsupportedOperationException
+ * if the addItemAfter method is not supported.
+ */
+ @Override
+ public Object addItemAfter(Object previousItemId)
+ throws UnsupportedOperationException {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Returns id of first item in the Container.
+ *
+ * @return ID of the first Item in the list.
+ */
+ @Override
+ public Object firstItemId() {
+ if (size < 1) {
+ return null;
+ }
+ return new Integer(1);
+ }
+
+ /**
+ * Returns <code>true</code> if given id is first id at first index.
+ *
+ * @param id
+ * ID of an Item in the Container.
+ */
+ @Override
+ public boolean isFirstId(Object id) {
+ return size > 0 && (id instanceof Integer)
+ && ((Integer) id).intValue() == 1;
+ }
+
+ /**
+ * Returns <code>true</code> if given id is last id at last index.
+ *
+ * @param id
+ * ID of an Item in the Container
+ *
+ */
+ @Override
+ public boolean isLastId(Object id) {
+ return size > 0 && (id instanceof Integer)
+ && ((Integer) id).intValue() == size;
+ }
+
+ /**
+ * Returns id of last item in the Container.
+ *
+ * @return ID of the last Item.
+ */
+ @Override
+ public Object lastItemId() {
+ if (size < 1) {
+ return null;
+ }
+ return new Integer(size);
+ }
+
+ /**
+ * Returns id of next item in container at next index.
+ *
+ * @param id
+ * ID of an Item in the Container.
+ * @return ID of the next Item or null.
+ */
+ @Override
+ public Object nextItemId(Object id) {
+ if (size < 1 || !(id instanceof Integer)) {
+ return null;
+ }
+ final int i = ((Integer) id).intValue();
+ if (i >= size) {
+ return null;
+ }
+ return new Integer(i + 1);
+ }
+
+ /**
+ * Returns id of previous item in container at previous index.
+ *
+ * @param id
+ * ID of an Item in the Container.
+ * @return ID of the previous Item or null.
+ */
+ @Override
+ public Object prevItemId(Object id) {
+ if (size < 1 || !(id instanceof Integer)) {
+ return null;
+ }
+ final int i = ((Integer) id).intValue();
+ if (i <= 1) {
+ return null;
+ }
+ return new Integer(i - 1);
+ }
+
+ /**
+ * The <code>Row</code> class implements methods of Item.
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @since 4.0
+ */
+ class Row implements Item {
+
+ Object id;
+
+ private Row(Object rowId) {
+ id = rowId;
+ }
+
+ /**
+ * Adds the item property.
+ *
+ * @param id
+ * ID of the new Property.
+ * @param property
+ * Property to be added and associated with ID.
+ * @return <code>true</code> if the operation succeeded;
+ * <code>false</code> otherwise.
+ * @throws UnsupportedOperationException
+ * if the addItemProperty method is not supported.
+ */
+ @Override
+ public boolean addItemProperty(Object id, Property property)
+ throws UnsupportedOperationException {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Gets the property corresponding to the given property ID stored in
+ * the Item.
+ *
+ * @param propertyId
+ * identifier of the Property to get
+ * @return the Property with the given ID or <code>null</code>
+ */
+ @Override
+ public Property<?> getItemProperty(Object propertyId) {
+ return getContainerProperty(id, propertyId);
+ }
+
+ /**
+ * Gets the collection of property IDs stored in the Item.
+ *
+ * @return unmodifiable collection containing IDs of the Properties
+ * stored the Item.
+ */
+ @Override
+ public Collection<String> getItemPropertyIds() {
+ return propertyIds;
+ }
+
+ /**
+ * Removes given item property.
+ *
+ * @param id
+ * ID of the Property to be removed.
+ * @return <code>true</code> if the item property is removed;
+ * <code>false</code> otherwise.
+ * @throws UnsupportedOperationException
+ * if the removeItemProperty is not supported.
+ */
+ @Override
+ public boolean removeItemProperty(Object id)
+ throws UnsupportedOperationException {
+ throw new UnsupportedOperationException();
+ }
+
+ }
+
+ /**
+ * Closes the statement.
+ *
+ * @see #close()
+ */
+ @Override
+ public void finalize() {
+ try {
+ close();
+ } catch (final SQLException ignored) {
+
+ }
+ }
+
+ /**
+ * Adds the given item at the position of given index.
+ *
+ * @param index
+ * Index to add the new item.
+ * @param newItemId
+ * Id of the new item to be added.
+ * @return new item or <code>null</code> if the operation fails.
+ * @throws UnsupportedOperationException
+ * if the addItemAt is not supported.
+ */
+ @Override
+ public Item addItemAt(int index, Object newItemId)
+ throws UnsupportedOperationException {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Adds item at the position of provided index in the container.
+ *
+ * @param index
+ * Index to add the new item.
+ * @return item id created new item or <code>null</code> if the operation
+ * fails.
+ *
+ * @throws UnsupportedOperationException
+ * if the addItemAt is not supported.
+ */
+
+ @Override
+ public Object addItemAt(int index) throws UnsupportedOperationException {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Gets the Index id in the container.
+ *
+ * @param index
+ * Index Id.
+ * @return ID in the given index.
+ */
+ @Override
+ public Object getIdByIndex(int index) {
+ if (size < 1 || index < 0 || index >= size) {
+ return null;
+ }
+ return new Integer(index + 1);
+ }
+
+ /**
+ * Gets the index of the Item corresponding to id in the container.
+ *
+ * @param id
+ * ID of an Item in the Container
+ * @return index of the Item, or -1 if the Container does not include the
+ * Item
+ */
+
+ @Override
+ public int indexOfId(Object id) {
+ if (size < 1 || !(id instanceof Integer)) {
+ return -1;
+ }
+ final int i = ((Integer) id).intValue();
+ if (i >= size || i < 1) {
+ return -1;
+ }
+ return i - 1;
+ }
+
+}
diff --git a/server/src/com/vaadin/data/util/TextFileProperty.java b/server/src/com/vaadin/data/util/TextFileProperty.java
new file mode 100644
index 0000000000..598b721a9c
--- /dev/null
+++ b/server/src/com/vaadin/data/util/TextFileProperty.java
@@ -0,0 +1,144 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.data.util;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+import java.nio.charset.Charset;
+
+/**
+ * Property implementation for wrapping a text file.
+ *
+ * Supports reading and writing of a File from/to String.
+ *
+ * {@link ValueChangeListener}s are supported, but only fire when
+ * setValue(Object) is explicitly called. {@link ReadOnlyStatusChangeListener}s
+ * are supported but only fire when setReadOnly(boolean) is explicitly called.
+ *
+ */
+@SuppressWarnings("serial")
+public class TextFileProperty extends AbstractProperty<String> {
+
+ private File file;
+ private Charset charset = null;
+
+ /**
+ * Wrap given file with property interface.
+ *
+ * Setting the file to null works, but getValue() will return null.
+ *
+ * @param file
+ * File to be wrapped.
+ */
+ public TextFileProperty(File file) {
+ this.file = file;
+ }
+
+ /**
+ * Wrap the given file with the property interface and specify character
+ * set.
+ *
+ * Setting the file to null works, but getValue() will return null.
+ *
+ * @param file
+ * File to be wrapped.
+ * @param charset
+ * Charset to be used for reading and writing the file.
+ */
+ public TextFileProperty(File file, Charset charset) {
+ this.file = file;
+ this.charset = charset;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Property#getType()
+ */
+ @Override
+ public Class<String> getType() {
+ return String.class;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Property#getValue()
+ */
+ @Override
+ public String getValue() {
+ if (file == null) {
+ return null;
+ }
+ try {
+ FileInputStream fis = new FileInputStream(file);
+ InputStreamReader isr = charset == null ? new InputStreamReader(fis)
+ : new InputStreamReader(fis, charset);
+ BufferedReader r = new BufferedReader(isr);
+ StringBuilder b = new StringBuilder();
+ char buf[] = new char[8 * 1024];
+ int len;
+ while ((len = r.read(buf)) != -1) {
+ b.append(buf, 0, len);
+ }
+ r.close();
+ isr.close();
+ fis.close();
+ return b.toString();
+ } catch (FileNotFoundException e) {
+ return null;
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Property#isReadOnly()
+ */
+ @Override
+ public boolean isReadOnly() {
+ return file == null || super.isReadOnly() || !file.canWrite();
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Property#setValue(java.lang.Object)
+ */
+ @Override
+ public void setValue(Object newValue) throws ReadOnlyException {
+ if (isReadOnly()) {
+ throw new ReadOnlyException();
+ }
+ if (file == null) {
+ return;
+ }
+
+ try {
+ FileOutputStream fos = new FileOutputStream(file);
+ OutputStreamWriter osw = charset == null ? new OutputStreamWriter(
+ fos) : new OutputStreamWriter(fos, charset);
+ BufferedWriter w = new BufferedWriter(osw);
+ w.append(newValue.toString());
+ w.flush();
+ w.close();
+ osw.close();
+ fos.close();
+ fireValueChange();
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+}
diff --git a/server/src/com/vaadin/data/util/TransactionalPropertyWrapper.java b/server/src/com/vaadin/data/util/TransactionalPropertyWrapper.java
new file mode 100644
index 0000000000..d042bfaac2
--- /dev/null
+++ b/server/src/com/vaadin/data/util/TransactionalPropertyWrapper.java
@@ -0,0 +1,114 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util;
+
+import com.vaadin.data.Property;
+import com.vaadin.data.Property.ValueChangeEvent;
+import com.vaadin.data.Property.ValueChangeNotifier;
+
+/**
+ * Wrapper class that helps implement two-phase commit for a non-transactional
+ * property.
+ *
+ * When accessing the property through the wrapper, getting and setting the
+ * property value take place immediately. However, the wrapper keeps track of
+ * the old value of the property so that it can be set for the property in case
+ * of a roll-back. This can result in the underlying property value changing
+ * multiple times (first based on modifications made by the application, then
+ * back upon roll-back).
+ *
+ * Value change events on the {@link TransactionalPropertyWrapper} are only
+ * fired at the end of a successful transaction, whereas listeners attached to
+ * the underlying property may receive multiple value change events.
+ *
+ * @see com.vaadin.data.Property.Transactional
+ *
+ * @author Vaadin Ltd
+ * @version @version@
+ * @since 7.0
+ *
+ * @param <T>
+ */
+public class TransactionalPropertyWrapper<T> extends AbstractProperty<T>
+ implements ValueChangeNotifier, Property.Transactional<T> {
+
+ private Property<T> wrappedProperty;
+ private boolean inTransaction = false;
+ private boolean valueChangePending;
+ private T valueBeforeTransaction;
+
+ public TransactionalPropertyWrapper(Property<T> wrappedProperty) {
+ this.wrappedProperty = wrappedProperty;
+ if (wrappedProperty instanceof ValueChangeNotifier) {
+ ((ValueChangeNotifier) wrappedProperty)
+ .addListener(new ValueChangeListener() {
+
+ @Override
+ public void valueChange(ValueChangeEvent event) {
+ fireValueChange();
+ }
+ });
+ }
+ }
+
+ @Override
+ public Class getType() {
+ return wrappedProperty.getType();
+ }
+
+ @Override
+ public T getValue() {
+ return wrappedProperty.getValue();
+ }
+
+ @Override
+ public void setValue(Object newValue) throws ReadOnlyException {
+ // Causes a value change to be sent to this listener which in turn fires
+ // a new value change event for this property
+ wrappedProperty.setValue(newValue);
+ }
+
+ @Override
+ public void startTransaction() {
+ inTransaction = true;
+ valueBeforeTransaction = getValue();
+ }
+
+ @Override
+ public void commit() {
+ endTransaction();
+ }
+
+ @Override
+ public void rollback() {
+ try {
+ wrappedProperty.setValue(valueBeforeTransaction);
+ } finally {
+ valueChangePending = false;
+ endTransaction();
+ }
+ }
+
+ protected void endTransaction() {
+ inTransaction = false;
+ valueBeforeTransaction = null;
+ if (valueChangePending) {
+ fireValueChange();
+ }
+ }
+
+ @Override
+ protected void fireValueChange() {
+ if (inTransaction) {
+ valueChangePending = true;
+ } else {
+ super.fireValueChange();
+ }
+ }
+
+ public Property<T> getWrappedProperty() {
+ return wrappedProperty;
+ }
+
+}
diff --git a/server/src/com/vaadin/data/util/VaadinPropertyDescriptor.java b/server/src/com/vaadin/data/util/VaadinPropertyDescriptor.java
new file mode 100644
index 0000000000..ee1e525540
--- /dev/null
+++ b/server/src/com/vaadin/data/util/VaadinPropertyDescriptor.java
@@ -0,0 +1,43 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util;
+
+import java.io.Serializable;
+
+import com.vaadin.data.Property;
+
+/**
+ * Property descriptor that can create a property instance for a bean.
+ *
+ * Used by {@link BeanItem} and {@link AbstractBeanContainer} to keep track of
+ * the set of properties of items.
+ *
+ * @param <BT>
+ * bean type
+ *
+ * @since 6.6
+ */
+public interface VaadinPropertyDescriptor<BT> extends Serializable {
+ /**
+ * Returns the name of the property.
+ *
+ * @return
+ */
+ public String getName();
+
+ /**
+ * Returns the type of the property.
+ *
+ * @return Class<?>
+ */
+ public Class<?> getPropertyType();
+
+ /**
+ * Creates a new {@link Property} instance for this property for a bean.
+ *
+ * @param bean
+ * @return
+ */
+ public Property<?> createProperty(BT bean);
+}
diff --git a/server/src/com/vaadin/data/util/converter/Converter.java b/server/src/com/vaadin/data/util/converter/Converter.java
new file mode 100644
index 0000000000..b8c15e8cdc
--- /dev/null
+++ b/server/src/com/vaadin/data/util/converter/Converter.java
@@ -0,0 +1,159 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.data.util.converter;
+
+import java.io.Serializable;
+import java.util.Locale;
+
+/**
+ * Interface that implements conversion between a model and a presentation type.
+ * <p>
+ * Typically {@link #convertToPresentation(Object, Locale)} and
+ * {@link #convertToModel(Object, Locale)} should be symmetric so that chaining
+ * these together returns the original result for all input but this is not a
+ * requirement.
+ * </p>
+ * <p>
+ * Converters must not have any side effects (never update UI from inside a
+ * converter).
+ * </p>
+ * <p>
+ * All Converters must be stateless and thread safe.
+ * </p>
+ * <p>
+ * If conversion of a value fails, a {@link ConversionException} is thrown.
+ * </p>
+ *
+ * @param <MODEL>
+ * The model type. Must be compatible with what
+ * {@link #getModelType()} returns.
+ * @param <PRESENTATION>
+ * The presentation type. Must be compatible with what
+ * {@link #getPresentationType()} returns.
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 7.0
+ */
+public interface Converter<PRESENTATION, MODEL> extends Serializable {
+
+ /**
+ * Converts the given value from target type to source type.
+ * <p>
+ * A converter can optionally use locale to do the conversion.
+ * </p>
+ * A converter should in most cases be symmetric so chaining
+ * {@link #convertToPresentation(Object, Locale)} and
+ * {@link #convertToModel(Object, Locale)} should return the original value.
+ *
+ * @param value
+ * The value to convert, compatible with the target type. Can be
+ * null
+ * @param locale
+ * The locale to use for conversion. Can be null.
+ * @return The converted value compatible with the source type
+ * @throws ConversionException
+ * If the value could not be converted
+ */
+ public MODEL convertToModel(PRESENTATION value, Locale locale)
+ throws ConversionException;
+
+ /**
+ * Converts the given value from source type to target type.
+ * <p>
+ * A converter can optionally use locale to do the conversion.
+ * </p>
+ * A converter should in most cases be symmetric so chaining
+ * {@link #convertToPresentation(Object, Locale)} and
+ * {@link #convertToModel(Object, Locale)} should return the original value.
+ *
+ * @param value
+ * The value to convert, compatible with the target type. Can be
+ * null
+ * @param locale
+ * The locale to use for conversion. Can be null.
+ * @return The converted value compatible with the source type
+ * @throws ConversionException
+ * If the value could not be converted
+ */
+ public PRESENTATION convertToPresentation(MODEL value, Locale locale)
+ throws ConversionException;
+
+ /**
+ * The source type of the converter.
+ *
+ * Values of this type can be passed to
+ * {@link #convertToPresentation(Object, Locale)}.
+ *
+ * @return The source type
+ */
+ public Class<MODEL> getModelType();
+
+ /**
+ * The target type of the converter.
+ *
+ * Values of this type can be passed to
+ * {@link #convertToModel(Object, Locale)}.
+ *
+ * @return The target type
+ */
+ public Class<PRESENTATION> getPresentationType();
+
+ /**
+ * An exception that signals that the value passed to
+ * {@link Converter#convertToPresentation(Object, Locale)} or
+ * {@link Converter#convertToModel(Object, Locale)} could not be converted.
+ *
+ * @author Vaadin Ltd
+ * @version
+ * @VERSION@
+ * @since 7.0
+ */
+ public static class ConversionException extends RuntimeException {
+
+ /**
+ * Constructs a new <code>ConversionException</code> without a detail
+ * message.
+ */
+ public ConversionException() {
+ }
+
+ /**
+ * Constructs a new <code>ConversionException</code> with the specified
+ * detail message.
+ *
+ * @param msg
+ * the detail message
+ */
+ public ConversionException(String msg) {
+ super(msg);
+ }
+
+ /**
+ * Constructs a new {@code ConversionException} with the specified
+ * cause.
+ *
+ * @param cause
+ * The cause of the the exception
+ */
+ public ConversionException(Throwable cause) {
+ super(cause);
+ }
+
+ /**
+ * Constructs a new <code>ConversionException</code> with the specified
+ * detail message and cause.
+ *
+ * @param message
+ * the detail message
+ * @param cause
+ * The cause of the the exception
+ */
+ public ConversionException(String message, Throwable cause) {
+ super(message, cause);
+ }
+ }
+
+}
diff --git a/server/src/com/vaadin/data/util/converter/ConverterFactory.java b/server/src/com/vaadin/data/util/converter/ConverterFactory.java
new file mode 100644
index 0000000000..ed4ab41ac0
--- /dev/null
+++ b/server/src/com/vaadin/data/util/converter/ConverterFactory.java
@@ -0,0 +1,23 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.data.util.converter;
+
+import java.io.Serializable;
+
+/**
+ * Factory interface for providing Converters based on a presentation type and a
+ * model type.
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 7.0
+ *
+ */
+public interface ConverterFactory extends Serializable {
+ public <PRESENTATION, MODEL> Converter<PRESENTATION, MODEL> createConverter(
+ Class<PRESENTATION> presentationType, Class<MODEL> modelType);
+
+}
diff --git a/server/src/com/vaadin/data/util/converter/ConverterUtil.java b/server/src/com/vaadin/data/util/converter/ConverterUtil.java
new file mode 100644
index 0000000000..7011496ed7
--- /dev/null
+++ b/server/src/com/vaadin/data/util/converter/ConverterUtil.java
@@ -0,0 +1,168 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util.converter;
+
+import java.io.Serializable;
+import java.util.Locale;
+
+import com.vaadin.Application;
+
+public class ConverterUtil implements Serializable {
+
+ /**
+ * Finds a converter that can convert from the given presentation type to
+ * the given model type and back. Uses the given application to find a
+ * {@link ConverterFactory} or, if application is null, uses the
+ * {@link Application#getCurrent()}.
+ *
+ * @param <PRESENTATIONTYPE>
+ * The presentation type
+ * @param <MODELTYPE>
+ * The model type
+ * @param presentationType
+ * The presentation type
+ * @param modelType
+ * The model type
+ * @param application
+ * The application to use to find a ConverterFactory or null to
+ * use the current application
+ * @return A Converter capable of converting between the given types or null
+ * if no converter was found
+ */
+ public static <PRESENTATIONTYPE, MODELTYPE> Converter<PRESENTATIONTYPE, MODELTYPE> getConverter(
+ Class<PRESENTATIONTYPE> presentationType,
+ Class<MODELTYPE> modelType, Application application) {
+ Converter<PRESENTATIONTYPE, MODELTYPE> converter = null;
+ if (application == null) {
+ application = Application.getCurrent();
+ }
+
+ if (application != null) {
+ ConverterFactory factory = application.getConverterFactory();
+ converter = factory.createConverter(presentationType, modelType);
+ }
+ return converter;
+
+ }
+
+ /**
+ * Convert the given value from the data source type to the UI type.
+ *
+ * @param modelValue
+ * The model value to convert
+ * @param presentationType
+ * The type of the presentation value
+ * @param converter
+ * The converter to (try to) use
+ * @param locale
+ * The locale to use for conversion
+ * @param <PRESENTATIONTYPE>
+ * Presentation type
+ *
+ * @return The converted value, compatible with the presentation type, or
+ * the original value if its type is compatible and no converter is
+ * set.
+ * @throws Converter.ConversionException
+ * if there is no converter and the type is not compatible with
+ * the model type.
+ */
+ @SuppressWarnings("unchecked")
+ public static <PRESENTATIONTYPE, MODELTYPE> PRESENTATIONTYPE convertFromModel(
+ MODELTYPE modelValue,
+ Class<? extends PRESENTATIONTYPE> presentationType,
+ Converter<PRESENTATIONTYPE, MODELTYPE> converter, Locale locale)
+ throws Converter.ConversionException {
+ if (converter != null) {
+ return converter.convertToPresentation(modelValue, locale);
+ }
+ if (modelValue == null) {
+ return null;
+ }
+
+ if (presentationType.isAssignableFrom(modelValue.getClass())) {
+ return (PRESENTATIONTYPE) modelValue;
+ } else {
+ throw new Converter.ConversionException(
+ "Unable to convert value of type "
+ + modelValue.getClass().getName()
+ + " to presentation type "
+ + presentationType
+ + ". No converter is set and the types are not compatible.");
+ }
+ }
+
+ /**
+ * @param <MODELTYPE>
+ * @param <PRESENTATIONTYPE>
+ * @param presentationValue
+ * @param modelType
+ * @param converter
+ * @param locale
+ * @return
+ * @throws Converter.ConversionException
+ */
+ public static <MODELTYPE, PRESENTATIONTYPE> MODELTYPE convertToModel(
+ PRESENTATIONTYPE presentationValue, Class<MODELTYPE> modelType,
+ Converter<PRESENTATIONTYPE, MODELTYPE> converter, Locale locale)
+ throws Converter.ConversionException {
+ if (converter != null) {
+ /*
+ * If there is a converter, always use it. It must convert or throw
+ * an exception.
+ */
+ return converter.convertToModel(presentationValue, locale);
+ }
+
+ if (presentationValue == null) {
+ // Null should always be passed through the converter but if there
+ // is no converter we can safely return null
+ return null;
+ }
+
+ if (modelType == null) {
+ // No model type, return original value
+ return (MODELTYPE) presentationValue;
+ } else if (modelType.isAssignableFrom(presentationValue.getClass())) {
+ // presentation type directly compatible with model type
+ return modelType.cast(presentationValue);
+ } else {
+ throw new Converter.ConversionException(
+ "Unable to convert value of type "
+ + presentationValue.getClass().getName()
+ + " to model type "
+ + modelType
+ + ". No converter is set and the types are not compatible.");
+ }
+
+ }
+
+ /**
+ * Checks if the given converter can handle conversion between the given
+ * presentation and model type
+ *
+ * @param converter
+ * The converter to check
+ * @param presentationType
+ * The presentation type
+ * @param modelType
+ * The model type
+ * @return true if the converter supports conversion between the given
+ * presentation and model type, false otherwise
+ */
+ public static boolean canConverterHandle(Converter<?, ?> converter,
+ Class<?> presentationType, Class<?> modelType) {
+ if (converter == null) {
+ return false;
+ }
+
+ if (!modelType.isAssignableFrom(converter.getModelType())) {
+ return false;
+ }
+ if (!presentationType.isAssignableFrom(converter.getPresentationType())) {
+ return false;
+ }
+
+ return true;
+ }
+}
diff --git a/server/src/com/vaadin/data/util/converter/DateToLongConverter.java b/server/src/com/vaadin/data/util/converter/DateToLongConverter.java
new file mode 100644
index 0000000000..aeba38aa1f
--- /dev/null
+++ b/server/src/com/vaadin/data/util/converter/DateToLongConverter.java
@@ -0,0 +1,72 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.data.util.converter;
+
+import java.util.Date;
+import java.util.Locale;
+
+/**
+ * A converter that converts from {@link Long} to {@link Date} and back.
+ *
+ * @author Vaadin Ltd
+ * @version
+ * @VERSION@
+ * @since 7.0
+ */
+public class DateToLongConverter implements Converter<Date, Long> {
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.data.util.converter.Converter#convertToModel(java.lang.Object,
+ * java.util.Locale)
+ */
+ @Override
+ public Long convertToModel(Date value, Locale locale) {
+ if (value == null) {
+ return null;
+ }
+
+ return value.getTime();
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.data.util.converter.Converter#convertToPresentation(java.lang
+ * .Object, java.util.Locale)
+ */
+ @Override
+ public Date convertToPresentation(Long value, Locale locale) {
+ if (value == null) {
+ return null;
+ }
+
+ return new Date(value);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.util.converter.Converter#getModelType()
+ */
+ @Override
+ public Class<Long> getModelType() {
+ return Long.class;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.util.converter.Converter#getPresentationType()
+ */
+ @Override
+ public Class<Date> getPresentationType() {
+ return Date.class;
+ }
+
+}
diff --git a/server/src/com/vaadin/data/util/converter/DefaultConverterFactory.java b/server/src/com/vaadin/data/util/converter/DefaultConverterFactory.java
new file mode 100644
index 0000000000..afb95d81ed
--- /dev/null
+++ b/server/src/com/vaadin/data/util/converter/DefaultConverterFactory.java
@@ -0,0 +1,101 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.data.util.converter;
+
+import java.util.Date;
+import java.util.logging.Logger;
+
+import com.vaadin.Application;
+
+/**
+ * Default implementation of {@link ConverterFactory}. Provides converters for
+ * standard types like {@link String}, {@link Double} and {@link Date}. </p>
+ * <p>
+ * Custom converters can be provided by extending this class and using
+ * {@link Application#setConverterFactory(ConverterFactory)}.
+ * </p>
+ *
+ * @author Vaadin Ltd
+ * @version
+ * @VERSION@
+ * @since 7.0
+ */
+public class DefaultConverterFactory implements ConverterFactory {
+
+ private final static Logger log = Logger
+ .getLogger(DefaultConverterFactory.class.getName());
+
+ @Override
+ public <PRESENTATION, MODEL> Converter<PRESENTATION, MODEL> createConverter(
+ Class<PRESENTATION> presentationType, Class<MODEL> modelType) {
+ Converter<PRESENTATION, MODEL> converter = findConverter(
+ presentationType, modelType);
+ if (converter != null) {
+ log.finest(getClass().getName() + " created a "
+ + converter.getClass());
+ return converter;
+ }
+
+ // Try to find a reverse converter
+ Converter<MODEL, PRESENTATION> reverseConverter = findConverter(
+ modelType, presentationType);
+ if (reverseConverter != null) {
+ log.finest(getClass().getName() + " created a reverse "
+ + reverseConverter.getClass());
+ return new ReverseConverter<PRESENTATION, MODEL>(reverseConverter);
+ }
+
+ log.finest(getClass().getName() + " could not find a converter for "
+ + presentationType.getName() + " to " + modelType.getName()
+ + " conversion");
+ return null;
+
+ }
+
+ protected <PRESENTATION, MODEL> Converter<PRESENTATION, MODEL> findConverter(
+ Class<PRESENTATION> presentationType, Class<MODEL> modelType) {
+ if (presentationType == String.class) {
+ // TextField converters and more
+ Converter<PRESENTATION, MODEL> converter = (Converter<PRESENTATION, MODEL>) createStringConverter(modelType);
+ if (converter != null) {
+ return converter;
+ }
+ } else if (presentationType == Date.class) {
+ // DateField converters and more
+ Converter<PRESENTATION, MODEL> converter = (Converter<PRESENTATION, MODEL>) createDateConverter(modelType);
+ if (converter != null) {
+ return converter;
+ }
+ }
+
+ return null;
+
+ }
+
+ protected Converter<Date, ?> createDateConverter(Class<?> sourceType) {
+ if (Long.class.isAssignableFrom(sourceType)) {
+ return new DateToLongConverter();
+ } else {
+ return null;
+ }
+ }
+
+ protected Converter<String, ?> createStringConverter(Class<?> sourceType) {
+ if (Double.class.isAssignableFrom(sourceType)) {
+ return new StringToDoubleConverter();
+ } else if (Integer.class.isAssignableFrom(sourceType)) {
+ return new StringToIntegerConverter();
+ } else if (Boolean.class.isAssignableFrom(sourceType)) {
+ return new StringToBooleanConverter();
+ } else if (Number.class.isAssignableFrom(sourceType)) {
+ return new StringToNumberConverter();
+ } else if (Date.class.isAssignableFrom(sourceType)) {
+ return new StringToDateConverter();
+ } else {
+ return null;
+ }
+ }
+
+}
diff --git a/server/src/com/vaadin/data/util/converter/ReverseConverter.java b/server/src/com/vaadin/data/util/converter/ReverseConverter.java
new file mode 100644
index 0000000000..fa1bb5daf1
--- /dev/null
+++ b/server/src/com/vaadin/data/util/converter/ReverseConverter.java
@@ -0,0 +1,84 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.data.util.converter;
+
+import java.util.Locale;
+
+/**
+ * A converter that wraps another {@link Converter} and reverses source and
+ * target types.
+ *
+ * @param <MODEL>
+ * The source type
+ * @param <PRESENTATION>
+ * The target type
+ *
+ * @author Vaadin Ltd
+ * @version
+ * @VERSION@
+ * @since 7.0
+ */
+public class ReverseConverter<PRESENTATION, MODEL> implements
+ Converter<PRESENTATION, MODEL> {
+
+ private Converter<MODEL, PRESENTATION> realConverter;
+
+ /**
+ * Creates a converter from source to target based on a converter that
+ * converts from target to source.
+ *
+ * @param converter
+ * The converter to use in a reverse fashion
+ */
+ public ReverseConverter(Converter<MODEL, PRESENTATION> converter) {
+ this.realConverter = converter;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.util.converter.Converter#convertToModel(java
+ * .lang.Object, java.util.Locale)
+ */
+ @Override
+ public MODEL convertToModel(PRESENTATION value, Locale locale)
+ throws com.vaadin.data.util.converter.Converter.ConversionException {
+ return realConverter.convertToPresentation(value, locale);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.data.util.converter.Converter#convertToPresentation(java.lang
+ * .Object, java.util.Locale)
+ */
+ @Override
+ public PRESENTATION convertToPresentation(MODEL value, Locale locale)
+ throws com.vaadin.data.util.converter.Converter.ConversionException {
+ return realConverter.convertToModel(value, locale);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.util.converter.Converter#getSourceType()
+ */
+ @Override
+ public Class<MODEL> getModelType() {
+ return realConverter.getPresentationType();
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.util.converter.Converter#getTargetType()
+ */
+ @Override
+ public Class<PRESENTATION> getPresentationType() {
+ return realConverter.getModelType();
+ }
+
+}
diff --git a/server/src/com/vaadin/data/util/converter/StringToBooleanConverter.java b/server/src/com/vaadin/data/util/converter/StringToBooleanConverter.java
new file mode 100644
index 0000000000..999f575dc4
--- /dev/null
+++ b/server/src/com/vaadin/data/util/converter/StringToBooleanConverter.java
@@ -0,0 +1,108 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.data.util.converter;
+
+import java.util.Locale;
+
+/**
+ * A converter that converts from {@link String} to {@link Boolean} and back.
+ * The String representation is given by Boolean.toString().
+ * <p>
+ * Leading and trailing white spaces are ignored when converting from a String.
+ * </p>
+ *
+ * @author Vaadin Ltd
+ * @version
+ * @VERSION@
+ * @since 7.0
+ */
+public class StringToBooleanConverter implements Converter<String, Boolean> {
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.data.util.converter.Converter#convertToModel(java.lang.Object,
+ * java.util.Locale)
+ */
+ @Override
+ public Boolean convertToModel(String value, Locale locale)
+ throws ConversionException {
+ if (value == null) {
+ return null;
+ }
+
+ // Remove leading and trailing white space
+ value = value.trim();
+
+ if (getTrueString().equals(value)) {
+ return true;
+ } else if (getFalseString().equals(value)) {
+ return false;
+ } else {
+ throw new ConversionException("Cannot convert " + value + " to "
+ + getModelType().getName());
+ }
+ }
+
+ /**
+ * Gets the string representation for true. Default is "true".
+ *
+ * @return the string representation for true
+ */
+ protected String getTrueString() {
+ return Boolean.TRUE.toString();
+ }
+
+ /**
+ * Gets the string representation for false. Default is "false".
+ *
+ * @return the string representation for false
+ */
+ protected String getFalseString() {
+ return Boolean.FALSE.toString();
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.data.util.converter.Converter#convertToPresentation(java.lang
+ * .Object, java.util.Locale)
+ */
+ @Override
+ public String convertToPresentation(Boolean value, Locale locale)
+ throws ConversionException {
+ if (value == null) {
+ return null;
+ }
+ if (value) {
+ return getTrueString();
+ } else {
+ return getFalseString();
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.util.converter.Converter#getModelType()
+ */
+ @Override
+ public Class<Boolean> getModelType() {
+ return Boolean.class;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.util.converter.Converter#getPresentationType()
+ */
+ @Override
+ public Class<String> getPresentationType() {
+ return String.class;
+ }
+
+}
diff --git a/server/src/com/vaadin/data/util/converter/StringToDateConverter.java b/server/src/com/vaadin/data/util/converter/StringToDateConverter.java
new file mode 100644
index 0000000000..487b02b2aa
--- /dev/null
+++ b/server/src/com/vaadin/data/util/converter/StringToDateConverter.java
@@ -0,0 +1,112 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.data.util.converter;
+
+import java.text.DateFormat;
+import java.text.ParsePosition;
+import java.util.Date;
+import java.util.Locale;
+
+/**
+ * A converter that converts from {@link Date} to {@link String} and back. Uses
+ * the given locale and {@link DateFormat} for formatting and parsing.
+ * <p>
+ * Leading and trailing white spaces are ignored when converting from a String.
+ * </p>
+ * <p>
+ * Override and overwrite {@link #getFormat(Locale)} to use a different format.
+ * </p>
+ *
+ * @author Vaadin Ltd
+ * @version
+ * @VERSION@
+ * @since 7.0
+ */
+public class StringToDateConverter implements Converter<String, Date> {
+
+ /**
+ * Returns the format used by {@link #convertToPresentation(Date, Locale)}
+ * and {@link #convertToModel(String, Locale)}.
+ *
+ * @param locale
+ * The locale to use
+ * @return A DateFormat instance
+ */
+ protected DateFormat getFormat(Locale locale) {
+ if (locale == null) {
+ locale = Locale.getDefault();
+ }
+
+ DateFormat f = DateFormat.getDateTimeInstance(DateFormat.MEDIUM,
+ DateFormat.MEDIUM, locale);
+ f.setLenient(false);
+ return f;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.data.util.converter.Converter#convertToModel(java.lang.Object,
+ * java.util.Locale)
+ */
+ @Override
+ public Date convertToModel(String value, Locale locale)
+ throws com.vaadin.data.util.converter.Converter.ConversionException {
+ if (value == null) {
+ return null;
+ }
+
+ // Remove leading and trailing white space
+ value = value.trim();
+
+ ParsePosition parsePosition = new ParsePosition(0);
+ Date parsedValue = getFormat(locale).parse(value, parsePosition);
+ if (parsePosition.getIndex() != value.length()) {
+ throw new ConversionException("Could not convert '" + value
+ + "' to " + getModelType().getName());
+ }
+
+ return parsedValue;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.data.util.converter.Converter#convertToPresentation(java.lang
+ * .Object, java.util.Locale)
+ */
+ @Override
+ public String convertToPresentation(Date value, Locale locale)
+ throws com.vaadin.data.util.converter.Converter.ConversionException {
+ if (value == null) {
+ return null;
+ }
+
+ return getFormat(locale).format(value);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.util.converter.Converter#getModelType()
+ */
+ @Override
+ public Class<Date> getModelType() {
+ return Date.class;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.util.converter.Converter#getPresentationType()
+ */
+ @Override
+ public Class<String> getPresentationType() {
+ return String.class;
+ }
+
+}
diff --git a/server/src/com/vaadin/data/util/converter/StringToDoubleConverter.java b/server/src/com/vaadin/data/util/converter/StringToDoubleConverter.java
new file mode 100644
index 0000000000..251f91855b
--- /dev/null
+++ b/server/src/com/vaadin/data/util/converter/StringToDoubleConverter.java
@@ -0,0 +1,107 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.data.util.converter;
+
+import java.text.NumberFormat;
+import java.text.ParsePosition;
+import java.util.Locale;
+
+/**
+ * A converter that converts from {@link String} to {@link Double} and back.
+ * Uses the given locale and a {@link NumberFormat} instance for formatting and
+ * parsing.
+ * <p>
+ * Leading and trailing white spaces are ignored when converting from a String.
+ * </p>
+ * <p>
+ * Override and overwrite {@link #getFormat(Locale)} to use a different format.
+ * </p>
+ *
+ * @author Vaadin Ltd
+ * @version
+ * @VERSION@
+ * @since 7.0
+ */
+public class StringToDoubleConverter implements Converter<String, Double> {
+
+ /**
+ * Returns the format used by {@link #convertToPresentation(Double, Locale)}
+ * and {@link #convertToModel(String, Locale)}.
+ *
+ * @param locale
+ * The locale to use
+ * @return A NumberFormat instance
+ */
+ protected NumberFormat getFormat(Locale locale) {
+ if (locale == null) {
+ locale = Locale.getDefault();
+ }
+
+ return NumberFormat.getNumberInstance(locale);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.data.util.converter.Converter#convertToModel(java.lang.Object,
+ * java.util.Locale)
+ */
+ @Override
+ public Double convertToModel(String value, Locale locale)
+ throws ConversionException {
+ if (value == null) {
+ return null;
+ }
+
+ // Remove leading and trailing white space
+ value = value.trim();
+
+ ParsePosition parsePosition = new ParsePosition(0);
+ Number parsedValue = getFormat(locale).parse(value, parsePosition);
+ if (parsePosition.getIndex() != value.length()) {
+ throw new ConversionException("Could not convert '" + value
+ + "' to " + getModelType().getName());
+ }
+ return parsedValue.doubleValue();
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.data.util.converter.Converter#convertToPresentation(java.lang
+ * .Object, java.util.Locale)
+ */
+ @Override
+ public String convertToPresentation(Double value, Locale locale)
+ throws ConversionException {
+ if (value == null) {
+ return null;
+ }
+
+ return getFormat(locale).format(value);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.util.converter.Converter#getModelType()
+ */
+ @Override
+ public Class<Double> getModelType() {
+ return Double.class;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.util.converter.Converter#getPresentationType()
+ */
+ @Override
+ public Class<String> getPresentationType() {
+ return String.class;
+ }
+}
diff --git a/server/src/com/vaadin/data/util/converter/StringToIntegerConverter.java b/server/src/com/vaadin/data/util/converter/StringToIntegerConverter.java
new file mode 100644
index 0000000000..950f01c6ab
--- /dev/null
+++ b/server/src/com/vaadin/data/util/converter/StringToIntegerConverter.java
@@ -0,0 +1,88 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.data.util.converter;
+
+import java.text.NumberFormat;
+import java.text.ParsePosition;
+import java.util.Locale;
+
+/**
+ * A converter that converts from {@link String} to {@link Integer} and back.
+ * Uses the given locale and a {@link NumberFormat} instance for formatting and
+ * parsing.
+ * <p>
+ * Override and overwrite {@link #getFormat(Locale)} to use a different format.
+ * </p>
+ *
+ * @author Vaadin Ltd
+ * @version
+ * @VERSION@
+ * @since 7.0
+ */
+public class StringToIntegerConverter implements Converter<String, Integer> {
+
+ /**
+ * Returns the format used by
+ * {@link #convertToPresentation(Integer, Locale)} and
+ * {@link #convertToModel(String, Locale)}.
+ *
+ * @param locale
+ * The locale to use
+ * @return A NumberFormat instance
+ */
+ protected NumberFormat getFormat(Locale locale) {
+ if (locale == null) {
+ locale = Locale.getDefault();
+ }
+ return NumberFormat.getIntegerInstance(locale);
+ }
+
+ @Override
+ public Integer convertToModel(String value, Locale locale)
+ throws ConversionException {
+ if (value == null) {
+ return null;
+ }
+
+ // Remove leading and trailing white space
+ value = value.trim();
+
+ // Parse and detect errors. If the full string was not used, it is
+ // an error.
+ ParsePosition parsePosition = new ParsePosition(0);
+ Number parsedValue = getFormat(locale).parse(value, parsePosition);
+ if (parsePosition.getIndex() != value.length()) {
+ throw new ConversionException("Could not convert '" + value
+ + "' to " + getModelType().getName());
+ }
+
+ if (parsedValue == null) {
+ // Convert "" to null
+ return null;
+ }
+ return parsedValue.intValue();
+ }
+
+ @Override
+ public String convertToPresentation(Integer value, Locale locale)
+ throws ConversionException {
+ if (value == null) {
+ return null;
+ }
+
+ return getFormat(locale).format(value);
+ }
+
+ @Override
+ public Class<Integer> getModelType() {
+ return Integer.class;
+ }
+
+ @Override
+ public Class<String> getPresentationType() {
+ return String.class;
+ }
+
+}
diff --git a/server/src/com/vaadin/data/util/converter/StringToNumberConverter.java b/server/src/com/vaadin/data/util/converter/StringToNumberConverter.java
new file mode 100644
index 0000000000..42699a326a
--- /dev/null
+++ b/server/src/com/vaadin/data/util/converter/StringToNumberConverter.java
@@ -0,0 +1,111 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.data.util.converter;
+
+import java.text.NumberFormat;
+import java.text.ParsePosition;
+import java.util.Locale;
+
+/**
+ * A converter that converts from {@link Number} to {@link String} and back.
+ * Uses the given locale and {@link NumberFormat} for formatting and parsing.
+ * <p>
+ * Override and overwrite {@link #getFormat(Locale)} to use a different format.
+ * </p>
+ *
+ * @author Vaadin Ltd
+ * @version
+ * @VERSION@
+ * @since 7.0
+ */
+public class StringToNumberConverter implements Converter<String, Number> {
+
+ /**
+ * Returns the format used by {@link #convertToPresentation(Number, Locale)}
+ * and {@link #convertToModel(String, Locale)}.
+ *
+ * @param locale
+ * The locale to use
+ * @return A NumberFormat instance
+ */
+ protected NumberFormat getFormat(Locale locale) {
+ if (locale == null) {
+ locale = Locale.getDefault();
+ }
+
+ return NumberFormat.getNumberInstance(locale);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.data.util.converter.Converter#convertToModel(java.lang.Object,
+ * java.util.Locale)
+ */
+ @Override
+ public Number convertToModel(String value, Locale locale)
+ throws ConversionException {
+ if (value == null) {
+ return null;
+ }
+
+ // Remove leading and trailing white space
+ value = value.trim();
+
+ // Parse and detect errors. If the full string was not used, it is
+ // an error.
+ ParsePosition parsePosition = new ParsePosition(0);
+ Number parsedValue = getFormat(locale).parse(value, parsePosition);
+ if (parsePosition.getIndex() != value.length()) {
+ throw new ConversionException("Could not convert '" + value
+ + "' to " + getModelType().getName());
+ }
+
+ if (parsedValue == null) {
+ // Convert "" to null
+ return null;
+ }
+ return parsedValue;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.data.util.converter.Converter#convertToPresentation(java.lang
+ * .Object, java.util.Locale)
+ */
+ @Override
+ public String convertToPresentation(Number value, Locale locale)
+ throws ConversionException {
+ if (value == null) {
+ return null;
+ }
+
+ return getFormat(locale).format(value);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.util.converter.Converter#getModelType()
+ */
+ @Override
+ public Class<Number> getModelType() {
+ return Number.class;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.util.converter.Converter#getPresentationType()
+ */
+ @Override
+ public Class<String> getPresentationType() {
+ return String.class;
+ }
+
+}
diff --git a/server/src/com/vaadin/data/util/filter/AbstractJunctionFilter.java b/server/src/com/vaadin/data/util/filter/AbstractJunctionFilter.java
new file mode 100644
index 0000000000..482b10120c
--- /dev/null
+++ b/server/src/com/vaadin/data/util/filter/AbstractJunctionFilter.java
@@ -0,0 +1,76 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util.filter;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+
+import com.vaadin.data.Container.Filter;
+
+/**
+ * Abstract base class for filters that are composed of multiple sub-filters.
+ *
+ * The method {@link #appliesToProperty(Object)} is provided to help
+ * implementing {@link Filter} for in-memory filters.
+ *
+ * @since 6.6
+ */
+public abstract class AbstractJunctionFilter implements Filter {
+
+ protected final Collection<Filter> filters;
+
+ public AbstractJunctionFilter(Filter... filters) {
+ this.filters = Collections.unmodifiableCollection(Arrays
+ .asList(filters));
+ }
+
+ /**
+ * Returns an unmodifiable collection of the sub-filters of this composite
+ * filter.
+ *
+ * @return
+ */
+ public Collection<Filter> getFilters() {
+ return filters;
+ }
+
+ /**
+ * Returns true if a change in the named property may affect the filtering
+ * result. If some of the sub-filters are not in-memory filters, true is
+ * returned.
+ *
+ * By default, all sub-filters are iterated to check if any of them applies.
+ * If there are no sub-filters, false is returned - override in subclasses
+ * to change this behavior.
+ */
+ @Override
+ public boolean appliesToProperty(Object propertyId) {
+ for (Filter filter : getFilters()) {
+ if (filter.appliesToProperty(propertyId)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == null || !getClass().equals(obj.getClass())) {
+ return false;
+ }
+ AbstractJunctionFilter other = (AbstractJunctionFilter) obj;
+ // contents comparison with equals()
+ return Arrays.equals(filters.toArray(), other.filters.toArray());
+ }
+
+ @Override
+ public int hashCode() {
+ int hash = getFilters().size();
+ for (Filter filter : filters) {
+ hash = (hash << 1) ^ filter.hashCode();
+ }
+ return hash;
+ }
+} \ No newline at end of file
diff --git a/server/src/com/vaadin/data/util/filter/And.java b/server/src/com/vaadin/data/util/filter/And.java
new file mode 100644
index 0000000000..ca6c35aba7
--- /dev/null
+++ b/server/src/com/vaadin/data/util/filter/And.java
@@ -0,0 +1,44 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util.filter;
+
+import com.vaadin.data.Container.Filter;
+import com.vaadin.data.Item;
+
+/**
+ * A compound {@link Filter} that accepts an item if all of its filters accept
+ * the item.
+ *
+ * If no filters are given, the filter should accept all items.
+ *
+ * This filter also directly supports in-memory filtering when all sub-filters
+ * do so.
+ *
+ * @see Or
+ *
+ * @since 6.6
+ */
+public final class And extends AbstractJunctionFilter {
+
+ /**
+ *
+ * @param filters
+ * filters of which the And filter will be composed
+ */
+ public And(Filter... filters) {
+ super(filters);
+ }
+
+ @Override
+ public boolean passesFilter(Object itemId, Item item)
+ throws UnsupportedFilterException {
+ for (Filter filter : getFilters()) {
+ if (!filter.passesFilter(itemId, item)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+}
diff --git a/server/src/com/vaadin/data/util/filter/Between.java b/server/src/com/vaadin/data/util/filter/Between.java
new file mode 100644
index 0000000000..b00a74d13d
--- /dev/null
+++ b/server/src/com/vaadin/data/util/filter/Between.java
@@ -0,0 +1,74 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+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;
+ }
+
+ @Override
+ 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;
+ }
+
+ @Override
+ 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/server/src/com/vaadin/data/util/filter/Compare.java b/server/src/com/vaadin/data/util/filter/Compare.java
new file mode 100644
index 0000000000..4091f5b922
--- /dev/null
+++ b/server/src/com/vaadin/data/util/filter/Compare.java
@@ -0,0 +1,327 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util.filter;
+
+import com.vaadin.data.Container.Filter;
+import com.vaadin.data.Item;
+import com.vaadin.data.Property;
+
+/**
+ * Simple container filter comparing an item property value against a given
+ * constant value. Use the nested classes {@link Equal}, {@link Greater},
+ * {@link Less}, {@link GreaterOrEqual} and {@link LessOrEqual} instead of this
+ * class directly.
+ *
+ * This filter also directly supports in-memory filtering.
+ *
+ * The reference and actual values must implement {@link Comparable} and the
+ * class of the actual property value must be assignable from the class of the
+ * reference value.
+ *
+ * @since 6.6
+ */
+public abstract class Compare implements Filter {
+
+ public enum Operation {
+ EQUAL, GREATER, LESS, GREATER_OR_EQUAL, LESS_OR_EQUAL
+ };
+
+ private final Object propertyId;
+ private final Operation operation;
+ private final Object value;
+
+ /**
+ * A {@link Compare} filter that accepts items for which the identified
+ * property value is equal to <code>value</code>.
+ *
+ * For in-memory filters, equals() is used for the comparison. For other
+ * containers, the comparison implementation is container dependent and may
+ * use e.g. database comparison operations.
+ *
+ * @since 6.6
+ */
+ public static final class Equal extends Compare {
+ /**
+ * Construct a filter that accepts items for which the identified
+ * property value is equal to <code>value</code>.
+ *
+ * For in-memory filters, equals() is used for the comparison. For other
+ * containers, the comparison implementation is container dependent and
+ * may use e.g. database comparison operations.
+ *
+ * @param propertyId
+ * the identifier of the property whose value to compare
+ * against value, not null
+ * @param value
+ * the value to compare against - null values may or may not
+ * be supported depending on the container
+ */
+ public Equal(Object propertyId, Object value) {
+ super(propertyId, value, Operation.EQUAL);
+ }
+ }
+
+ /**
+ * A {@link Compare} filter that accepts items for which the identified
+ * property value is greater than <code>value</code>.
+ *
+ * For in-memory filters, the values must implement {@link Comparable} and
+ * {@link Comparable#compareTo(Object)} is used for the comparison. For
+ * other containers, the comparison implementation is container dependent
+ * and may use e.g. database comparison operations.
+ *
+ * @since 6.6
+ */
+ public static final class Greater extends Compare {
+ /**
+ * Construct a filter that accepts items for which the identified
+ * property value is greater than <code>value</code>.
+ *
+ * For in-memory filters, the values must implement {@link Comparable}
+ * and {@link Comparable#compareTo(Object)} is used for the comparison.
+ * For other containers, the comparison implementation is container
+ * dependent and may use e.g. database comparison operations.
+ *
+ * @param propertyId
+ * the identifier of the property whose value to compare
+ * against value, not null
+ * @param value
+ * the value to compare against - null values may or may not
+ * be supported depending on the container
+ */
+ public Greater(Object propertyId, Object value) {
+ super(propertyId, value, Operation.GREATER);
+ }
+ }
+
+ /**
+ * A {@link Compare} filter that accepts items for which the identified
+ * property value is less than <code>value</code>.
+ *
+ * For in-memory filters, the values must implement {@link Comparable} and
+ * {@link Comparable#compareTo(Object)} is used for the comparison. For
+ * other containers, the comparison implementation is container dependent
+ * and may use e.g. database comparison operations.
+ *
+ * @since 6.6
+ */
+ public static final class Less extends Compare {
+ /**
+ * Construct a filter that accepts items for which the identified
+ * property value is less than <code>value</code>.
+ *
+ * For in-memory filters, the values must implement {@link Comparable}
+ * and {@link Comparable#compareTo(Object)} is used for the comparison.
+ * For other containers, the comparison implementation is container
+ * dependent and may use e.g. database comparison operations.
+ *
+ * @param propertyId
+ * the identifier of the property whose value to compare
+ * against value, not null
+ * @param value
+ * the value to compare against - null values may or may not
+ * be supported depending on the container
+ */
+ public Less(Object propertyId, Object value) {
+ super(propertyId, value, Operation.LESS);
+ }
+ }
+
+ /**
+ * A {@link Compare} filter that accepts items for which the identified
+ * property value is greater than or equal to <code>value</code>.
+ *
+ * For in-memory filters, the values must implement {@link Comparable} and
+ * {@link Comparable#compareTo(Object)} is used for the comparison. For
+ * other containers, the comparison implementation is container dependent
+ * and may use e.g. database comparison operations.
+ *
+ * @since 6.6
+ */
+ public static final class GreaterOrEqual extends Compare {
+ /**
+ * Construct a filter that accepts items for which the identified
+ * property value is greater than or equal to <code>value</code>.
+ *
+ * For in-memory filters, the values must implement {@link Comparable}
+ * and {@link Comparable#compareTo(Object)} is used for the comparison.
+ * For other containers, the comparison implementation is container
+ * dependent and may use e.g. database comparison operations.
+ *
+ * @param propertyId
+ * the identifier of the property whose value to compare
+ * against value, not null
+ * @param value
+ * the value to compare against - null values may or may not
+ * be supported depending on the container
+ */
+ public GreaterOrEqual(Object propertyId, Object value) {
+ super(propertyId, value, Operation.GREATER_OR_EQUAL);
+ }
+ }
+
+ /**
+ * A {@link Compare} filter that accepts items for which the identified
+ * property value is less than or equal to <code>value</code>.
+ *
+ * For in-memory filters, the values must implement {@link Comparable} and
+ * {@link Comparable#compareTo(Object)} is used for the comparison. For
+ * other containers, the comparison implementation is container dependent
+ * and may use e.g. database comparison operations.
+ *
+ * @since 6.6
+ */
+ public static final class LessOrEqual extends Compare {
+ /**
+ * Construct a filter that accepts items for which the identified
+ * property value is less than or equal to <code>value</code>.
+ *
+ * For in-memory filters, the values must implement {@link Comparable}
+ * and {@link Comparable#compareTo(Object)} is used for the comparison.
+ * For other containers, the comparison implementation is container
+ * dependent and may use e.g. database comparison operations.
+ *
+ * @param propertyId
+ * the identifier of the property whose value to compare
+ * against value, not null
+ * @param value
+ * the value to compare against - null values may or may not
+ * be supported depending on the container
+ */
+ public LessOrEqual(Object propertyId, Object value) {
+ super(propertyId, value, Operation.LESS_OR_EQUAL);
+ }
+ }
+
+ /**
+ * Constructor for a {@link Compare} filter that compares the value of an
+ * item property with the given constant <code>value</code>.
+ *
+ * This constructor is intended to be used by the nested static classes only
+ * ({@link Equal}, {@link Greater}, {@link Less}, {@link GreaterOrEqual},
+ * {@link LessOrEqual}).
+ *
+ * For in-memory filtering, comparisons except EQUAL require that the values
+ * implement {@link Comparable} and {@link Comparable#compareTo(Object)} is
+ * used for the comparison. The equality comparison is performed using
+ * {@link Object#equals(Object)}.
+ *
+ * For other containers, the comparison implementation is container
+ * dependent and may use e.g. database comparison operations. Therefore, the
+ * behavior of comparisons might differ in some cases between in-memory and
+ * other containers.
+ *
+ * @param propertyId
+ * the identifier of the property whose value to compare against
+ * value, not null
+ * @param value
+ * the value to compare against - null values may or may not be
+ * supported depending on the container
+ * @param operation
+ * the comparison {@link Operation} to use
+ */
+ Compare(Object propertyId, Object value, Operation operation) {
+ this.propertyId = propertyId;
+ this.value = value;
+ this.operation = operation;
+ }
+
+ @Override
+ public boolean passesFilter(Object itemId, Item item) {
+ final Property<?> p = item.getItemProperty(getPropertyId());
+ if (null == p) {
+ return false;
+ }
+ Object value = p.getValue();
+ switch (getOperation()) {
+ case EQUAL:
+ return (null == this.value) ? (null == value) : this.value
+ .equals(value);
+ case GREATER:
+ return compareValue(value) > 0;
+ case LESS:
+ return compareValue(value) < 0;
+ case GREATER_OR_EQUAL:
+ return compareValue(value) >= 0;
+ case LESS_OR_EQUAL:
+ return compareValue(value) <= 0;
+ }
+ // all cases should have been processed above
+ return false;
+ }
+
+ @SuppressWarnings({ "unchecked", "rawtypes" })
+ protected int compareValue(Object value1) {
+ if (null == value) {
+ return null == value1 ? 0 : -1;
+ } else if (null == value1) {
+ return 1;
+ } else if (getValue() instanceof Comparable
+ && value1.getClass().isAssignableFrom(getValue().getClass())) {
+ return -((Comparable) getValue()).compareTo(value1);
+ }
+ throw new IllegalArgumentException("Could not compare the arguments: "
+ + value1 + ", " + getValue());
+ }
+
+ @Override
+ public boolean appliesToProperty(Object propertyId) {
+ return getPropertyId().equals(propertyId);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+
+ // Only objects of the same class can be equal
+ if (!getClass().equals(obj.getClass())) {
+ return false;
+ }
+ final Compare o = (Compare) obj;
+
+ // Checks the properties one by one
+ if (getPropertyId() != o.getPropertyId() && null != o.getPropertyId()
+ && !o.getPropertyId().equals(getPropertyId())) {
+ return false;
+ }
+ if (getOperation() != o.getOperation()) {
+ return false;
+ }
+ return (null == getValue()) ? null == o.getValue() : getValue().equals(
+ o.getValue());
+ }
+
+ @Override
+ public int hashCode() {
+ return (null != getPropertyId() ? getPropertyId().hashCode() : 0)
+ ^ (null != getValue() ? getValue().hashCode() : 0);
+ }
+
+ /**
+ * Returns the property id of the property to compare against the fixed
+ * value.
+ *
+ * @return property id (not null)
+ */
+ public Object getPropertyId() {
+ return propertyId;
+ }
+
+ /**
+ * Returns the comparison operation.
+ *
+ * @return {@link Operation}
+ */
+ public Operation getOperation() {
+ return operation;
+ }
+
+ /**
+ * Returns the value to compare the property against.
+ *
+ * @return comparison reference value
+ */
+ public Object getValue() {
+ return value;
+ }
+}
diff --git a/server/src/com/vaadin/data/util/filter/IsNull.java b/server/src/com/vaadin/data/util/filter/IsNull.java
new file mode 100644
index 0000000000..3faf4153ee
--- /dev/null
+++ b/server/src/com/vaadin/data/util/filter/IsNull.java
@@ -0,0 +1,79 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util.filter;
+
+import com.vaadin.data.Container.Filter;
+import com.vaadin.data.Item;
+import com.vaadin.data.Property;
+
+/**
+ * Simple container filter checking whether an item property value is null.
+ *
+ * This filter also directly supports in-memory filtering.
+ *
+ * @since 6.6
+ */
+public final class IsNull implements Filter {
+
+ private final Object propertyId;
+
+ /**
+ * Constructor for a filter that compares the value of an item property with
+ * null.
+ *
+ * For in-memory filtering, a simple == check is performed. For other
+ * containers, the comparison implementation is container dependent but
+ * should correspond to the in-memory null check.
+ *
+ * @param propertyId
+ * the identifier (not null) of the property whose value to check
+ */
+ public IsNull(Object propertyId) {
+ this.propertyId = propertyId;
+ }
+
+ @Override
+ public boolean passesFilter(Object itemId, Item item)
+ throws UnsupportedOperationException {
+ final Property<?> p = item.getItemProperty(getPropertyId());
+ if (null == p) {
+ return false;
+ }
+ return null == p.getValue();
+ }
+
+ @Override
+ public boolean appliesToProperty(Object propertyId) {
+ return getPropertyId().equals(propertyId);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ // Only objects of the same class can be equal
+ if (!getClass().equals(obj.getClass())) {
+ return false;
+ }
+ final IsNull o = (IsNull) obj;
+
+ // Checks the properties one by one
+ return (null != getPropertyId()) ? getPropertyId().equals(
+ o.getPropertyId()) : null == o.getPropertyId();
+ }
+
+ @Override
+ public int hashCode() {
+ return (null != getPropertyId() ? getPropertyId().hashCode() : 0);
+ }
+
+ /**
+ * Returns the property id of the property tested by the filter, not null
+ * for valid filters.
+ *
+ * @return property id (not null)
+ */
+ public Object getPropertyId() {
+ return propertyId;
+ }
+
+}
diff --git a/server/src/com/vaadin/data/util/filter/Like.java b/server/src/com/vaadin/data/util/filter/Like.java
new file mode 100644
index 0000000000..3dcc48e809
--- /dev/null
+++ b/server/src/com/vaadin/data/util/filter/Like.java
@@ -0,0 +1,83 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+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;
+ }
+
+ @Override
+ 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());
+ }
+
+ @Override
+ 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/server/src/com/vaadin/data/util/filter/Not.java b/server/src/com/vaadin/data/util/filter/Not.java
new file mode 100644
index 0000000000..bbfc9ca86a
--- /dev/null
+++ b/server/src/com/vaadin/data/util/filter/Not.java
@@ -0,0 +1,70 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util.filter;
+
+import com.vaadin.data.Container.Filter;
+import com.vaadin.data.Item;
+
+/**
+ * Negating filter that accepts the items rejected by another filter.
+ *
+ * This filter directly supports in-memory filtering when the negated filter
+ * does so.
+ *
+ * @since 6.6
+ */
+public final class Not implements Filter {
+ private final Filter filter;
+
+ /**
+ * Constructs a filter that negates a filter.
+ *
+ * @param filter
+ * {@link Filter} to negate, not-null
+ */
+ public Not(Filter filter) {
+ this.filter = filter;
+ }
+
+ /**
+ * Returns the negated filter.
+ *
+ * @return Filter
+ */
+ public Filter getFilter() {
+ return filter;
+ }
+
+ @Override
+ public boolean passesFilter(Object itemId, Item item)
+ throws UnsupportedOperationException {
+ return !filter.passesFilter(itemId, item);
+ }
+
+ /**
+ * Returns true if a change in the named property may affect the filtering
+ * result. Return value is the same as {@link #appliesToProperty(Object)}
+ * for the negated filter.
+ *
+ * @return boolean
+ */
+ @Override
+ public boolean appliesToProperty(Object propertyId) {
+ return filter.appliesToProperty(propertyId);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == null || !getClass().equals(obj.getClass())) {
+ return false;
+ }
+ return filter.equals(((Not) obj).getFilter());
+ }
+
+ @Override
+ public int hashCode() {
+ return filter.hashCode();
+ }
+
+}
diff --git a/server/src/com/vaadin/data/util/filter/Or.java b/server/src/com/vaadin/data/util/filter/Or.java
new file mode 100644
index 0000000000..b60074f7e3
--- /dev/null
+++ b/server/src/com/vaadin/data/util/filter/Or.java
@@ -0,0 +1,63 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util.filter;
+
+import com.vaadin.data.Container.Filter;
+import com.vaadin.data.Item;
+
+/**
+ * A compound {@link Filter} that accepts an item if any of its filters accept
+ * the item.
+ *
+ * If no filters are given, the filter should reject all items.
+ *
+ * This filter also directly supports in-memory filtering when all sub-filters
+ * do so.
+ *
+ * @see And
+ *
+ * @since 6.6
+ */
+public final class Or extends AbstractJunctionFilter {
+
+ /**
+ *
+ * @param filters
+ * filters of which the Or filter will be composed
+ */
+ public Or(Filter... filters) {
+ super(filters);
+ }
+
+ @Override
+ public boolean passesFilter(Object itemId, Item item)
+ throws UnsupportedFilterException {
+ for (Filter filter : getFilters()) {
+ if (filter.passesFilter(itemId, item)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns true if a change in the named property may affect the filtering
+ * result. If some of the sub-filters are not in-memory filters, true is
+ * returned.
+ *
+ * By default, all sub-filters are iterated to check if any of them applies.
+ * If there are no sub-filters, true is returned as an empty Or rejects all
+ * items.
+ */
+ @Override
+ public boolean appliesToProperty(Object propertyId) {
+ if (getFilters().isEmpty()) {
+ // empty Or filters out everything
+ return true;
+ } else {
+ return super.appliesToProperty(propertyId);
+ }
+ }
+
+}
diff --git a/server/src/com/vaadin/data/util/filter/SimpleStringFilter.java b/server/src/com/vaadin/data/util/filter/SimpleStringFilter.java
new file mode 100644
index 0000000000..f98b2c02b4
--- /dev/null
+++ b/server/src/com/vaadin/data/util/filter/SimpleStringFilter.java
@@ -0,0 +1,152 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util.filter;
+
+import com.vaadin.data.Container.Filter;
+import com.vaadin.data.Item;
+import com.vaadin.data.Property;
+
+/**
+ * Simple string filter for matching items that start with or contain a
+ * specified string. The matching can be case-sensitive or case-insensitive.
+ *
+ * This filter also directly supports in-memory filtering. When performing
+ * in-memory filtering, values of other types are converted using toString(),
+ * but other (lazy container) implementations do not need to perform such
+ * conversions and might not support values of different types.
+ *
+ * Note that this filter is modeled after the pre-6.6 filtering mechanisms, and
+ * might not be very efficient e.g. for database filtering.
+ *
+ * TODO this might still change
+ *
+ * @since 6.6
+ */
+public final class SimpleStringFilter implements Filter {
+
+ final Object propertyId;
+ final String filterString;
+ final boolean ignoreCase;
+ final boolean onlyMatchPrefix;
+
+ public SimpleStringFilter(Object propertyId, String filterString,
+ boolean ignoreCase, boolean onlyMatchPrefix) {
+ this.propertyId = propertyId;
+ this.filterString = ignoreCase ? filterString.toLowerCase()
+ : filterString;
+ this.ignoreCase = ignoreCase;
+ this.onlyMatchPrefix = onlyMatchPrefix;
+ }
+
+ @Override
+ public boolean passesFilter(Object itemId, Item item) {
+ final Property<?> p = item.getItemProperty(propertyId);
+ if (p == null) {
+ return false;
+ }
+ Object propertyValue = p.getValue();
+ if (propertyValue == null) {
+ return false;
+ }
+ final String value = ignoreCase ? propertyValue.toString()
+ .toLowerCase() : propertyValue.toString();
+ if (onlyMatchPrefix) {
+ if (!value.startsWith(filterString)) {
+ return false;
+ }
+ } else {
+ if (!value.contains(filterString)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public boolean appliesToProperty(Object propertyId) {
+ return this.propertyId.equals(propertyId);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+
+ // Only ones of the objects of the same class can be equal
+ if (!(obj instanceof SimpleStringFilter)) {
+ return false;
+ }
+ final SimpleStringFilter o = (SimpleStringFilter) obj;
+
+ // Checks the properties one by one
+ if (propertyId != o.propertyId && o.propertyId != null
+ && !o.propertyId.equals(propertyId)) {
+ return false;
+ }
+ if (filterString != o.filterString && o.filterString != null
+ && !o.filterString.equals(filterString)) {
+ return false;
+ }
+ if (ignoreCase != o.ignoreCase) {
+ return false;
+ }
+ if (onlyMatchPrefix != o.onlyMatchPrefix) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ return (propertyId != null ? propertyId.hashCode() : 0)
+ ^ (filterString != null ? filterString.hashCode() : 0);
+ }
+
+ /**
+ * Returns the property identifier to which this filter applies.
+ *
+ * @return property id
+ */
+ public Object getPropertyId() {
+ return propertyId;
+ }
+
+ /**
+ * Returns the filter string.
+ *
+ * Note: this method is intended only for implementations of lazy string
+ * filters and may change in the future.
+ *
+ * @return filter string given to the constructor
+ */
+ public String getFilterString() {
+ return filterString;
+ }
+
+ /**
+ * Returns whether the filter is case-insensitive or case-sensitive.
+ *
+ * Note: this method is intended only for implementations of lazy string
+ * filters and may change in the future.
+ *
+ * @return true if performing case-insensitive filtering, false for
+ * case-sensitive
+ */
+ public boolean isIgnoreCase() {
+ return ignoreCase;
+ }
+
+ /**
+ * Returns true if the filter only applies to the beginning of the value
+ * string, false for any location in the value.
+ *
+ * Note: this method is intended only for implementations of lazy string
+ * filters and may change in the future.
+ *
+ * @return true if checking for matches at the beginning of the value only,
+ * false if matching any part of value
+ */
+ public boolean isOnlyMatchPrefix() {
+ return onlyMatchPrefix;
+ }
+}
diff --git a/server/src/com/vaadin/data/util/filter/UnsupportedFilterException.java b/server/src/com/vaadin/data/util/filter/UnsupportedFilterException.java
new file mode 100644
index 0000000000..c09cc474e9
--- /dev/null
+++ b/server/src/com/vaadin/data/util/filter/UnsupportedFilterException.java
@@ -0,0 +1,35 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util.filter;
+
+import java.io.Serializable;
+
+/**
+ * Exception for cases where a container does not support a specific type of
+ * filters.
+ *
+ * If possible, this should be thrown already when adding a filter to a
+ * container. If a problem is not detected at that point, an
+ * {@link UnsupportedOperationException} can be throws when attempting to
+ * perform filtering.
+ *
+ * @since 6.6
+ */
+public class UnsupportedFilterException extends RuntimeException implements
+ Serializable {
+ public UnsupportedFilterException() {
+ }
+
+ public UnsupportedFilterException(String message) {
+ super(message);
+ }
+
+ public UnsupportedFilterException(Exception cause) {
+ super(cause);
+ }
+
+ public UnsupportedFilterException(String message, Exception cause) {
+ super(message, cause);
+ }
+} \ No newline at end of file
diff --git a/server/src/com/vaadin/data/util/package.html b/server/src/com/vaadin/data/util/package.html
new file mode 100644
index 0000000000..07e3acde9e
--- /dev/null
+++ b/server/src/com/vaadin/data/util/package.html
@@ -0,0 +1,18 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
+<html>
+<head>
+
+</head>
+
+<body bgcolor="white">
+
+<p>Provides implementations of Property, Item and Container
+interfaces, and utilities for the data layer.</p>
+
+<p>Various Property, Item and Container implementations are provided
+in this package. Each implementation can have its own sets of
+constraints on the data it encapsulates and on how the implementation
+can be used. See the class javadocs for more information.</p>
+
+</body>
+</html>
diff --git a/server/src/com/vaadin/data/util/sqlcontainer/CacheFlushNotifier.java b/server/src/com/vaadin/data/util/sqlcontainer/CacheFlushNotifier.java
new file mode 100644
index 0000000000..788966048d
--- /dev/null
+++ b/server/src/com/vaadin/data/util/sqlcontainer/CacheFlushNotifier.java
@@ -0,0 +1,92 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util.sqlcontainer;
+
+import java.io.Serializable;
+import java.lang.ref.ReferenceQueue;
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.List;
+
+import com.vaadin.data.util.sqlcontainer.query.FreeformQuery;
+import com.vaadin.data.util.sqlcontainer.query.QueryDelegate;
+import com.vaadin.data.util.sqlcontainer.query.TableQuery;
+
+/**
+ * CacheFlushNotifier is a simple static notification mechanism to inform other
+ * SQLContainers that the contents of their caches may have become stale.
+ */
+class CacheFlushNotifier implements Serializable {
+ /*
+ * SQLContainer instance reference list and dead reference queue. Used for
+ * the cache flush notification feature.
+ */
+ private static List<WeakReference<SQLContainer>> allInstances = new ArrayList<WeakReference<SQLContainer>>();
+ private static ReferenceQueue<SQLContainer> deadInstances = new ReferenceQueue<SQLContainer>();
+
+ /**
+ * Adds the given SQLContainer to the cache flush notification receiver list
+ *
+ * @param c
+ * Container to add
+ */
+ public static void addInstance(SQLContainer c) {
+ removeDeadReferences();
+ if (c != null) {
+ allInstances.add(new WeakReference<SQLContainer>(c, deadInstances));
+ }
+ }
+
+ /**
+ * Removes dead references from instance list
+ */
+ private static void removeDeadReferences() {
+ java.lang.ref.Reference<? extends SQLContainer> dead = deadInstances
+ .poll();
+ while (dead != null) {
+ allInstances.remove(dead);
+ dead = deadInstances.poll();
+ }
+ }
+
+ /**
+ * Iterates through the instances and notifies containers which are
+ * connected to the same table or are using the same query string.
+ *
+ * @param c
+ * SQLContainer that issued the cache flush notification
+ */
+ public static void notifyOfCacheFlush(SQLContainer c) {
+ removeDeadReferences();
+ for (WeakReference<SQLContainer> wr : allInstances) {
+ if (wr.get() != null) {
+ SQLContainer wrc = wr.get();
+ if (wrc == null) {
+ continue;
+ }
+ /*
+ * If the reference points to the container sending the
+ * notification, do nothing.
+ */
+ if (wrc.equals(c)) {
+ continue;
+ }
+ /* Compare QueryDelegate types and tableName/queryString */
+ QueryDelegate wrQd = wrc.getQueryDelegate();
+ QueryDelegate qd = c.getQueryDelegate();
+ if (wrQd instanceof TableQuery
+ && qd instanceof TableQuery
+ && ((TableQuery) wrQd).getTableName().equals(
+ ((TableQuery) qd).getTableName())) {
+ wrc.refresh();
+ } else if (wrQd instanceof FreeformQuery
+ && qd instanceof FreeformQuery
+ && ((FreeformQuery) wrQd).getQueryString().equals(
+ ((FreeformQuery) qd).getQueryString())) {
+ wrc.refresh();
+ }
+ }
+ }
+ }
+}
diff --git a/server/src/com/vaadin/data/util/sqlcontainer/CacheMap.java b/server/src/com/vaadin/data/util/sqlcontainer/CacheMap.java
new file mode 100644
index 0000000000..839fceb3c2
--- /dev/null
+++ b/server/src/com/vaadin/data/util/sqlcontainer/CacheMap.java
@@ -0,0 +1,31 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util.sqlcontainer;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * CacheMap extends LinkedHashMap, adding the possibility to adjust maximum
+ * number of items. In SQLContainer this is used for RowItem -cache. Cache size
+ * will be two times the page length parameter of the container.
+ */
+class CacheMap<K, V> extends LinkedHashMap<K, V> {
+ private static final long serialVersionUID = 679999766473555231L;
+ private int cacheLimit = SQLContainer.CACHE_RATIO
+ * SQLContainer.DEFAULT_PAGE_LENGTH;
+
+ @Override
+ protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
+ return size() > cacheLimit;
+ }
+
+ void setCacheLimit(int limit) {
+ cacheLimit = limit > 0 ? limit : SQLContainer.DEFAULT_PAGE_LENGTH;
+ }
+
+ int getCacheLimit() {
+ return cacheLimit;
+ }
+} \ No newline at end of file
diff --git a/server/src/com/vaadin/data/util/sqlcontainer/ColumnProperty.java b/server/src/com/vaadin/data/util/sqlcontainer/ColumnProperty.java
new file mode 100644
index 0000000000..168bce1880
--- /dev/null
+++ b/server/src/com/vaadin/data/util/sqlcontainer/ColumnProperty.java
@@ -0,0 +1,248 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util.sqlcontainer;
+
+import java.sql.Date;
+import java.sql.Time;
+import java.sql.Timestamp;
+
+import com.vaadin.data.Property;
+
+/**
+ * ColumnProperty represents the value of one column in a RowItem. In addition
+ * to the value, ColumnProperty also contains some basic column attributes such
+ * as nullability status, read-only status and data type.
+ *
+ * Note that depending on the QueryDelegate in use this does not necessarily map
+ * into an actual column in a database table.
+ */
+final public class ColumnProperty implements Property {
+ private static final long serialVersionUID = -3694463129581802457L;
+
+ private RowItem owner;
+
+ private String propertyId;
+
+ private boolean readOnly;
+ private boolean allowReadOnlyChange = true;
+ private boolean nullable = true;
+
+ private Object value;
+ private Object changedValue;
+ private Class<?> type;
+
+ private boolean modified;
+
+ private boolean versionColumn;
+
+ /**
+ * Prevent instantiation without required parameters.
+ */
+ @SuppressWarnings("unused")
+ private ColumnProperty() {
+ }
+
+ public ColumnProperty(String propertyId, boolean readOnly,
+ boolean allowReadOnlyChange, boolean nullable, Object value,
+ Class<?> type) {
+ if (propertyId == null) {
+ throw new IllegalArgumentException("Properties must be named.");
+ }
+ if (type == null) {
+ throw new IllegalArgumentException("Property type must be set.");
+ }
+ this.propertyId = propertyId;
+ this.type = type;
+ this.value = value;
+
+ this.allowReadOnlyChange = allowReadOnlyChange;
+ this.nullable = nullable;
+ this.readOnly = readOnly;
+ }
+
+ @Override
+ public Object getValue() {
+ if (isModified()) {
+ return changedValue;
+ }
+ return value;
+ }
+
+ @Override
+ public void setValue(Object newValue) throws ReadOnlyException {
+ if (newValue == null && !nullable) {
+ throw new NotNullableException(
+ "Null values are not allowed for this property.");
+ }
+ if (readOnly) {
+ throw new ReadOnlyException(
+ "Cannot set value for read-only property.");
+ }
+
+ /* Check if this property is a date property. */
+ boolean isDateProperty = Time.class.equals(getType())
+ || Date.class.equals(getType())
+ || Timestamp.class.equals(getType());
+
+ if (newValue != null) {
+ /* Handle SQL dates, times and Timestamps given as java.util.Date */
+ if (isDateProperty) {
+ /*
+ * Try to get the millisecond value from the new value of this
+ * property. Possible type to convert from is java.util.Date.
+ */
+ long millis = 0;
+ if (newValue instanceof java.util.Date) {
+ millis = ((java.util.Date) newValue).getTime();
+ /*
+ * Create the new object based on the millisecond value,
+ * according to the type of this property.
+ */
+ if (Time.class.equals(getType())) {
+ newValue = new Time(millis);
+ } else if (Date.class.equals(getType())) {
+ newValue = new Date(millis);
+ } else if (Timestamp.class.equals(getType())) {
+ newValue = new Timestamp(millis);
+ }
+ }
+ }
+
+ if (!getType().isAssignableFrom(newValue.getClass())) {
+ throw new IllegalArgumentException(
+ "Illegal value type for ColumnProperty");
+ }
+
+ /*
+ * If the value to be set is the same that has already been set, do
+ * not set it again.
+ */
+ if (isValueAlreadySet(newValue)) {
+ return;
+ }
+ }
+
+ /* Set the new value and notify container of the change. */
+ changedValue = newValue;
+ modified = true;
+ owner.getContainer().itemChangeNotification(owner);
+ }
+
+ private boolean isValueAlreadySet(Object newValue) {
+ Object referenceValue = isModified() ? changedValue : value;
+
+ return (isNullable() && newValue == null && referenceValue == null)
+ || newValue.equals(referenceValue);
+ }
+
+ @Override
+ public Class<?> getType() {
+ return type;
+ }
+
+ @Override
+ public boolean isReadOnly() {
+ return readOnly;
+ }
+
+ public boolean isReadOnlyChangeAllowed() {
+ return allowReadOnlyChange;
+ }
+
+ @Override
+ public void setReadOnly(boolean newStatus) {
+ if (allowReadOnlyChange) {
+ readOnly = newStatus;
+ }
+ }
+
+ public String getPropertyId() {
+ return propertyId;
+ }
+
+ /**
+ * Returns the value of the Property in human readable textual format.
+ *
+ * @see java.lang.Object#toString()
+ * @deprecated get the string representation from the value
+ */
+ @Deprecated
+ @Override
+ public String toString() {
+ throw new UnsupportedOperationException(
+ "Use ColumnProperty.getValue() instead of ColumnProperty.toString()");
+ }
+
+ public void setOwner(RowItem owner) {
+ if (owner == null) {
+ throw new IllegalArgumentException("Owner can not be set to null.");
+ }
+ if (this.owner != null) {
+ throw new IllegalStateException(
+ "ColumnProperties can only be bound once.");
+ }
+ this.owner = owner;
+ }
+
+ public boolean isModified() {
+ return modified;
+ }
+
+ public boolean isVersionColumn() {
+ return versionColumn;
+ }
+
+ public void setVersionColumn(boolean versionColumn) {
+ this.versionColumn = versionColumn;
+ }
+
+ public boolean isNullable() {
+ return nullable;
+ }
+
+ /**
+ * An exception that signals that a <code>null</code> value was passed to
+ * the <code>setValue</code> method, but the value of this property can not
+ * be set to <code>null</code>.
+ */
+ @SuppressWarnings("serial")
+ public class NotNullableException extends RuntimeException {
+
+ /**
+ * Constructs a new <code>NotNullableException</code> without a detail
+ * message.
+ */
+ public NotNullableException() {
+ }
+
+ /**
+ * Constructs a new <code>NotNullableException</code> with the specified
+ * detail message.
+ *
+ * @param msg
+ * the detail message
+ */
+ public NotNullableException(String msg) {
+ super(msg);
+ }
+
+ /**
+ * Constructs a new <code>NotNullableException</code> from another
+ * exception.
+ *
+ * @param cause
+ * The cause of the failure
+ */
+ public NotNullableException(Throwable cause) {
+ super(cause);
+ }
+ }
+
+ public void commit() {
+ if (isModified()) {
+ modified = false;
+ value = changedValue;
+ }
+ }
+}
diff --git a/server/src/com/vaadin/data/util/sqlcontainer/OptimisticLockException.java b/server/src/com/vaadin/data/util/sqlcontainer/OptimisticLockException.java
new file mode 100644
index 0000000000..adfd439ac8
--- /dev/null
+++ b/server/src/com/vaadin/data/util/sqlcontainer/OptimisticLockException.java
@@ -0,0 +1,38 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util.sqlcontainer;
+
+import com.vaadin.data.util.sqlcontainer.query.TableQuery;
+
+/**
+ * An OptimisticLockException is thrown when trying to update or delete a row
+ * that has been changed since last read from the database.
+ *
+ * OptimisticLockException is a runtime exception because optimistic locking is
+ * turned off by default, and as such will never be thrown in a default
+ * configuration. In order to turn on optimistic locking, you need to specify
+ * the version column in your TableQuery instance.
+ *
+ * @see TableQuery#setVersionColumn(String)
+ *
+ * @author Jonatan Kronqvist / Vaadin Ltd
+ */
+public class OptimisticLockException extends RuntimeException {
+
+ private final RowId rowId;
+
+ public OptimisticLockException(RowId rowId) {
+ super();
+ this.rowId = rowId;
+ }
+
+ public OptimisticLockException(String msg, RowId rowId) {
+ super(msg);
+ this.rowId = rowId;
+ }
+
+ public RowId getRowId() {
+ return rowId;
+ }
+}
diff --git a/server/src/com/vaadin/data/util/sqlcontainer/ReadOnlyRowId.java b/server/src/com/vaadin/data/util/sqlcontainer/ReadOnlyRowId.java
new file mode 100644
index 0000000000..c73ffce63a
--- /dev/null
+++ b/server/src/com/vaadin/data/util/sqlcontainer/ReadOnlyRowId.java
@@ -0,0 +1,31 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util.sqlcontainer;
+
+public class ReadOnlyRowId extends RowId {
+ private static final long serialVersionUID = -2626764781642012467L;
+ private final Integer rowNum;
+
+ public ReadOnlyRowId(int rowNum) {
+ super();
+ this.rowNum = rowNum;
+ }
+
+ @Override
+ public int hashCode() {
+ return rowNum.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == null || !(obj instanceof ReadOnlyRowId)) {
+ return false;
+ }
+ return rowNum.equals(((ReadOnlyRowId) obj).rowNum);
+ }
+
+ public int getRowNum() {
+ return rowNum;
+ }
+}
diff --git a/server/src/com/vaadin/data/util/sqlcontainer/Reference.java b/server/src/com/vaadin/data/util/sqlcontainer/Reference.java
new file mode 100644
index 0000000000..dea1aa87c0
--- /dev/null
+++ b/server/src/com/vaadin/data/util/sqlcontainer/Reference.java
@@ -0,0 +1,56 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util.sqlcontainer;
+
+import java.io.Serializable;
+
+/**
+ * The reference class represents a simple [usually foreign key] reference to
+ * another SQLContainer. Actual foreign key reference in the database is not
+ * required, but it is recommended to make sure that certain constraints are
+ * followed.
+ */
+@SuppressWarnings("serial")
+class Reference implements Serializable {
+
+ /**
+ * The SQLContainer that this reference points to.
+ */
+ private SQLContainer referencedContainer;
+
+ /**
+ * The column ID/name in the referencing SQLContainer that contains the key
+ * used for the reference.
+ */
+ private String referencingColumn;
+
+ /**
+ * The column ID/name in the referenced SQLContainer that contains the key
+ * used for the reference.
+ */
+ private String referencedColumn;
+
+ /**
+ * Constructs a new reference to be used within the SQLContainer to
+ * reference another SQLContainer.
+ */
+ Reference(SQLContainer referencedContainer, String referencingColumn,
+ String referencedColumn) {
+ this.referencedContainer = referencedContainer;
+ this.referencingColumn = referencingColumn;
+ this.referencedColumn = referencedColumn;
+ }
+
+ SQLContainer getReferencedContainer() {
+ return referencedContainer;
+ }
+
+ String getReferencingColumn() {
+ return referencingColumn;
+ }
+
+ String getReferencedColumn() {
+ return referencedColumn;
+ }
+}
diff --git a/server/src/com/vaadin/data/util/sqlcontainer/RowId.java b/server/src/com/vaadin/data/util/sqlcontainer/RowId.java
new file mode 100644
index 0000000000..925325134a
--- /dev/null
+++ b/server/src/com/vaadin/data/util/sqlcontainer/RowId.java
@@ -0,0 +1,81 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util.sqlcontainer;
+
+import java.io.Serializable;
+
+/**
+ * RowId represents identifiers of a single database result set row.
+ *
+ * The data structure of a RowId is an Object array which contains the values of
+ * the primary key columns of the identified row. This allows easy equals()
+ * -comparison of RowItems.
+ */
+public class RowId implements Serializable {
+ private static final long serialVersionUID = -3161778404698901258L;
+ protected Object[] id;
+
+ /**
+ * Prevent instantiation without required parameters.
+ */
+ protected RowId() {
+ }
+
+ public RowId(Object[] id) {
+ if (id == null) {
+ throw new IllegalArgumentException("id parameter must not be null!");
+ }
+ this.id = id;
+ }
+
+ public Object[] getId() {
+ return id;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = 31;
+ if (id != null) {
+ for (Object o : id) {
+ if (o != null) {
+ result += o.hashCode();
+ }
+ }
+ }
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == null || !(obj instanceof RowId)) {
+ return false;
+ }
+ Object[] compId = ((RowId) obj).getId();
+ if (id == null && compId == null) {
+ return true;
+ }
+ if (id.length != compId.length) {
+ return false;
+ }
+ for (int i = 0; i < id.length; i++) {
+ if ((id[i] == null && compId[i] != null)
+ || (id[i] != null && !id[i].equals(compId[i]))) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ StringBuffer s = new StringBuffer();
+ for (int i = 0; i < id.length; i++) {
+ s.append(id[i]);
+ if (i < id.length - 1) {
+ s.append("/");
+ }
+ }
+ return s.toString();
+ }
+}
diff --git a/server/src/com/vaadin/data/util/sqlcontainer/RowItem.java b/server/src/com/vaadin/data/util/sqlcontainer/RowItem.java
new file mode 100644
index 0000000000..d613a06b63
--- /dev/null
+++ b/server/src/com/vaadin/data/util/sqlcontainer/RowItem.java
@@ -0,0 +1,133 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util.sqlcontainer;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+
+import com.vaadin.data.Item;
+import com.vaadin.data.Property;
+
+/**
+ * RowItem represents one row of a result set obtained from a QueryDelegate.
+ *
+ * Note that depending on the QueryDelegate in use this does not necessarily map
+ * into an actual row in a database table.
+ */
+public final class RowItem implements Item {
+ private static final long serialVersionUID = -6228966439127951408L;
+ private SQLContainer container;
+ private RowId id;
+ private Collection<ColumnProperty> properties;
+
+ /**
+ * Prevent instantiation without required parameters.
+ */
+ @SuppressWarnings("unused")
+ private RowItem() {
+ }
+
+ public RowItem(SQLContainer container, RowId id,
+ Collection<ColumnProperty> properties) {
+ if (container == null) {
+ throw new IllegalArgumentException("Container cannot be null.");
+ }
+ if (id == null) {
+ throw new IllegalArgumentException("Row ID cannot be null.");
+ }
+ this.container = container;
+ this.properties = properties;
+ /* Set this RowItem as owner to the properties */
+ if (properties != null) {
+ for (ColumnProperty p : properties) {
+ p.setOwner(this);
+ }
+ }
+ this.id = id;
+ }
+
+ @Override
+ public Property<?> getItemProperty(Object id) {
+ if (id instanceof String && id != null) {
+ for (ColumnProperty cp : properties) {
+ if (id.equals(cp.getPropertyId())) {
+ return cp;
+ }
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public Collection<?> getItemPropertyIds() {
+ Collection<String> ids = new ArrayList<String>(properties.size());
+ for (ColumnProperty cp : properties) {
+ ids.add(cp.getPropertyId());
+ }
+ return Collections.unmodifiableCollection(ids);
+ }
+
+ /**
+ * Adding properties is not supported. Properties are generated by
+ * SQLContainer.
+ */
+ @Override
+ public boolean addItemProperty(Object id, Property property)
+ throws UnsupportedOperationException {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Removing properties is not supported. Properties are generated by
+ * SQLContainer.
+ */
+ @Override
+ public boolean removeItemProperty(Object id)
+ throws UnsupportedOperationException {
+ throw new UnsupportedOperationException();
+ }
+
+ public RowId getId() {
+ return id;
+ }
+
+ public SQLContainer getContainer() {
+ return container;
+ }
+
+ public boolean isModified() {
+ if (properties != null) {
+ for (ColumnProperty p : properties) {
+ if (p.isModified()) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public String toString() {
+ StringBuffer s = new StringBuffer();
+ s.append("ID:");
+ s.append(getId().toString());
+ for (Object propId : getItemPropertyIds()) {
+ s.append("|");
+ s.append(propId.toString());
+ s.append(":");
+ Object value = getItemProperty(propId).getValue();
+ s.append((null != value) ? value.toString() : null);
+ }
+ return s.toString();
+ }
+
+ public void commit() {
+ if (properties != null) {
+ for (ColumnProperty p : properties) {
+ p.commit();
+ }
+ }
+ }
+}
diff --git a/server/src/com/vaadin/data/util/sqlcontainer/SQLContainer.java b/server/src/com/vaadin/data/util/sqlcontainer/SQLContainer.java
new file mode 100644
index 0000000000..5827390723
--- /dev/null
+++ b/server/src/com/vaadin/data/util/sqlcontainer/SQLContainer.java
@@ -0,0 +1,1716 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util.sqlcontainer;
+
+import java.io.IOException;
+import java.sql.ResultSet;
+import java.sql.ResultSetMetaData;
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.ConcurrentModificationException;
+import java.util.Date;
+import java.util.EventObject;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import com.vaadin.data.Container;
+import com.vaadin.data.Item;
+import com.vaadin.data.Property;
+import com.vaadin.data.util.filter.Compare.Equal;
+import com.vaadin.data.util.filter.Like;
+import com.vaadin.data.util.filter.UnsupportedFilterException;
+import com.vaadin.data.util.sqlcontainer.query.OrderBy;
+import com.vaadin.data.util.sqlcontainer.query.QueryDelegate;
+import com.vaadin.data.util.sqlcontainer.query.QueryDelegate.RowIdChangeListener;
+import com.vaadin.data.util.sqlcontainer.query.TableQuery;
+import com.vaadin.data.util.sqlcontainer.query.generator.MSSQLGenerator;
+import com.vaadin.data.util.sqlcontainer.query.generator.OracleGenerator;
+
+public class SQLContainer implements Container, Container.Filterable,
+ Container.Indexed, Container.Sortable, Container.ItemSetChangeNotifier {
+
+ /** Query delegate */
+ private QueryDelegate delegate;
+ /** Auto commit mode, default = false */
+ private boolean autoCommit = false;
+
+ /** Page length = number of items contained in one page */
+ private int pageLength = DEFAULT_PAGE_LENGTH;
+ public static final int DEFAULT_PAGE_LENGTH = 100;
+
+ /** Number of items to cache = CACHE_RATIO x pageLength */
+ public static final int CACHE_RATIO = 2;
+
+ /** Item and index caches */
+ private final Map<Integer, RowId> itemIndexes = new HashMap<Integer, RowId>();
+ private final CacheMap<RowId, RowItem> cachedItems = new CacheMap<RowId, RowItem>();
+
+ /** Container properties = column names, data types and statuses */
+ private final List<String> propertyIds = new ArrayList<String>();
+ private final Map<String, Class<?>> propertyTypes = new HashMap<String, Class<?>>();
+ private final Map<String, Boolean> propertyReadOnly = new HashMap<String, Boolean>();
+ private final Map<String, Boolean> propertyNullable = new HashMap<String, Boolean>();
+
+ /** Filters (WHERE) and sorters (ORDER BY) */
+ private final List<Filter> filters = new ArrayList<Filter>();
+ private final List<OrderBy> sorters = new ArrayList<OrderBy>();
+
+ /**
+ * Total number of items available in the data source using the current
+ * query, filters and sorters.
+ */
+ private int size;
+
+ /**
+ * Size updating logic. Do not update size from data source if it has been
+ * updated in the last sizeValidMilliSeconds milliseconds.
+ */
+ private final int sizeValidMilliSeconds = 10000;
+ private boolean sizeDirty = true;
+ private Date sizeUpdated = new Date();
+
+ /** Starting row number of the currently fetched page */
+ private int currentOffset;
+
+ /** ItemSetChangeListeners */
+ private LinkedList<Container.ItemSetChangeListener> itemSetChangeListeners;
+
+ /** Temporary storage for modified items and items to be removed and added */
+ private final Map<RowId, RowItem> removedItems = new HashMap<RowId, RowItem>();
+ private final List<RowItem> addedItems = new ArrayList<RowItem>();
+ private final List<RowItem> modifiedItems = new ArrayList<RowItem>();
+
+ /** List of references to other SQLContainers */
+ private final Map<SQLContainer, Reference> references = new HashMap<SQLContainer, Reference>();
+
+ /** Cache flush notification system enabled. Disabled by default. */
+ private boolean notificationsEnabled;
+
+ /**
+ * Prevent instantiation without a QueryDelegate.
+ */
+ @SuppressWarnings("unused")
+ private SQLContainer() {
+ }
+
+ /**
+ * Creates and initializes SQLContainer using the given QueryDelegate
+ *
+ * @param delegate
+ * QueryDelegate implementation
+ * @throws SQLException
+ */
+ public SQLContainer(QueryDelegate delegate) throws SQLException {
+ if (delegate == null) {
+ throw new IllegalArgumentException(
+ "QueryDelegate must not be null.");
+ }
+ this.delegate = delegate;
+ getPropertyIds();
+ cachedItems.setCacheLimit(CACHE_RATIO * getPageLength());
+ }
+
+ /**************************************/
+ /** Methods from interface Container **/
+ /**************************************/
+
+ /**
+ * Note! If auto commit mode is enabled, this method will still return the
+ * temporary row ID assigned for the item. Implement
+ * QueryDelegate.RowIdChangeListener to receive the actual Row ID value
+ * after the addition has been committed.
+ *
+ * {@inheritDoc}
+ */
+
+ @Override
+ public Object addItem() throws UnsupportedOperationException {
+ Object emptyKey[] = new Object[delegate.getPrimaryKeyColumns().size()];
+ RowId itemId = new TemporaryRowId(emptyKey);
+ // Create new empty column properties for the row item.
+ List<ColumnProperty> itemProperties = new ArrayList<ColumnProperty>();
+ for (String propertyId : propertyIds) {
+ /* Default settings for new item properties. */
+ itemProperties
+ .add(new ColumnProperty(propertyId, propertyReadOnly
+ .get(propertyId),
+ !propertyReadOnly.get(propertyId), propertyNullable
+ .get(propertyId), null, getType(propertyId)));
+ }
+ RowItem newRowItem = new RowItem(this, itemId, itemProperties);
+
+ if (autoCommit) {
+ /* Add and commit instantly */
+ try {
+ if (delegate instanceof TableQuery) {
+ itemId = ((TableQuery) delegate)
+ .storeRowImmediately(newRowItem);
+ } else {
+ delegate.beginTransaction();
+ delegate.storeRow(newRowItem);
+ delegate.commit();
+ }
+ refresh();
+ if (notificationsEnabled) {
+ CacheFlushNotifier.notifyOfCacheFlush(this);
+ }
+ getLogger().log(Level.FINER, "Row added to DB...");
+ return itemId;
+ } catch (SQLException e) {
+ getLogger().log(Level.WARNING,
+ "Failed to add row to DB. Rolling back.", e);
+ try {
+ delegate.rollback();
+ } catch (SQLException ee) {
+ getLogger().log(Level.SEVERE,
+ "Failed to roll back row addition", e);
+ }
+ return null;
+ }
+ } else {
+ addedItems.add(newRowItem);
+ fireContentsChange();
+ return itemId;
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container#containsId(java.lang.Object)
+ */
+
+ @Override
+ public boolean containsId(Object itemId) {
+ if (itemId == null) {
+ return false;
+ }
+
+ if (cachedItems.containsKey(itemId)) {
+ return true;
+ } else {
+ for (RowItem item : addedItems) {
+ if (item.getId().equals(itemId)) {
+ return itemPassesFilters(item);
+ }
+ }
+ }
+ if (removedItems.containsKey(itemId)) {
+ return false;
+ }
+
+ if (itemId instanceof ReadOnlyRowId) {
+ int rowNum = ((ReadOnlyRowId) itemId).getRowNum();
+ return rowNum >= 0 && rowNum < size;
+ }
+
+ if (itemId instanceof RowId && !(itemId instanceof TemporaryRowId)) {
+ try {
+ return delegate.containsRowWithKey(((RowId) itemId).getId());
+ } catch (Exception e) {
+ /* Query failed, just return false. */
+ getLogger().log(Level.WARNING, "containsId query failed", e);
+ }
+ }
+ return false;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container#getContainerProperty(java.lang.Object,
+ * java.lang.Object)
+ */
+
+ @Override
+ public Property<?> getContainerProperty(Object itemId, Object propertyId) {
+ Item item = getItem(itemId);
+ if (item == null) {
+ return null;
+ }
+ return item.getItemProperty(propertyId);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container#getContainerPropertyIds()
+ */
+
+ @Override
+ public Collection<?> getContainerPropertyIds() {
+ return Collections.unmodifiableCollection(propertyIds);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container#getItem(java.lang.Object)
+ */
+
+ @Override
+ public Item getItem(Object itemId) {
+ if (!cachedItems.containsKey(itemId)) {
+ int index = indexOfId(itemId);
+ if (index >= size) {
+ // The index is in the added items
+ int offset = index - size;
+ RowItem item = addedItems.get(offset);
+ if (itemPassesFilters(item)) {
+ return item;
+ } else {
+ return null;
+ }
+ } else {
+ // load the item into cache
+ updateOffsetAndCache(index);
+ }
+ }
+ return cachedItems.get(itemId);
+ }
+
+ /**
+ * Bypasses in-memory filtering to return items that are cached in memory.
+ * <em>NOTE</em>: This does not bypass database-level filtering.
+ *
+ * @param itemId
+ * the id of the item to retrieve.
+ * @return the item represented by itemId.
+ */
+ public Item getItemUnfiltered(Object itemId) {
+ if (!cachedItems.containsKey(itemId)) {
+ for (RowItem item : addedItems) {
+ if (item.getId().equals(itemId)) {
+ return item;
+ }
+ }
+ }
+ return cachedItems.get(itemId);
+ }
+
+ /**
+ * NOTE! Do not use this method if in any way avoidable. This method doesn't
+ * (and cannot) use lazy loading, which means that all rows in the database
+ * will be loaded into memory.
+ *
+ * {@inheritDoc}
+ */
+
+ @Override
+ public Collection<?> getItemIds() {
+ updateCount();
+ ArrayList<RowId> ids = new ArrayList<RowId>();
+ ResultSet rs = null;
+ try {
+ // Load ALL rows :(
+ delegate.beginTransaction();
+ rs = delegate.getResults(0, 0);
+ List<String> pKeys = delegate.getPrimaryKeyColumns();
+ while (rs.next()) {
+ RowId id = null;
+ if (pKeys.isEmpty()) {
+ /* Create a read only itemId */
+ id = new ReadOnlyRowId(rs.getRow());
+ } else {
+ /* Generate itemId for the row based on primary key(s) */
+ Object[] itemId = new Object[pKeys.size()];
+ for (int i = 0; i < pKeys.size(); i++) {
+ itemId[i] = rs.getObject(pKeys.get(i));
+ }
+ id = new RowId(itemId);
+ }
+ if (id != null && !removedItems.containsKey(id)) {
+ ids.add(id);
+ }
+ }
+ rs.getStatement().close();
+ rs.close();
+ delegate.commit();
+ } catch (SQLException e) {
+ getLogger().log(Level.WARNING,
+ "getItemIds() failed, rolling back.", e);
+ try {
+ delegate.rollback();
+ } catch (SQLException e1) {
+ getLogger().log(Level.SEVERE, "Failed to roll back state", e1);
+ }
+ try {
+ rs.getStatement().close();
+ rs.close();
+ } catch (SQLException e1) {
+ getLogger().log(Level.WARNING, "Closing session failed", e1);
+ }
+ throw new RuntimeException("Failed to fetch item indexes.", e);
+ }
+ for (RowItem item : getFilteredAddedItems()) {
+ ids.add(item.getId());
+ }
+ return Collections.unmodifiableCollection(ids);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container#getType(java.lang.Object)
+ */
+
+ @Override
+ public Class<?> getType(Object propertyId) {
+ if (!propertyIds.contains(propertyId)) {
+ return null;
+ }
+ return propertyTypes.get(propertyId);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container#size()
+ */
+
+ @Override
+ public int size() {
+ updateCount();
+ return size + sizeOfAddedItems() - removedItems.size();
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container#removeItem(java.lang.Object)
+ */
+
+ @Override
+ public boolean removeItem(Object itemId)
+ throws UnsupportedOperationException {
+ if (!containsId(itemId)) {
+ return false;
+ }
+ for (RowItem item : addedItems) {
+ if (item.getId().equals(itemId)) {
+ addedItems.remove(item);
+ fireContentsChange();
+ return true;
+ }
+ }
+
+ if (autoCommit) {
+ /* Remove and commit instantly. */
+ Item i = getItem(itemId);
+ if (i == null) {
+ return false;
+ }
+ try {
+ delegate.beginTransaction();
+ boolean success = delegate.removeRow((RowItem) i);
+ delegate.commit();
+ refresh();
+ if (notificationsEnabled) {
+ CacheFlushNotifier.notifyOfCacheFlush(this);
+ }
+ if (success) {
+ getLogger().log(Level.FINER, "Row removed from DB...");
+ }
+ return success;
+ } catch (SQLException e) {
+ getLogger().log(Level.WARNING,
+ "Failed to remove row, rolling back", e);
+ try {
+ delegate.rollback();
+ } catch (SQLException ee) {
+ /* Nothing can be done here */
+ getLogger().log(Level.SEVERE,
+ "Failed to rollback row removal", ee);
+ }
+ return false;
+ } catch (OptimisticLockException e) {
+ getLogger().log(Level.WARNING,
+ "Failed to remove row, rolling back", e);
+ try {
+ delegate.rollback();
+ } catch (SQLException ee) {
+ /* Nothing can be done here */
+ getLogger().log(Level.SEVERE,
+ "Failed to rollback row removal", ee);
+ }
+ throw e;
+ }
+ } else {
+ removedItems.put((RowId) itemId, (RowItem) getItem(itemId));
+ cachedItems.remove(itemId);
+ refresh();
+ return true;
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container#removeAllItems()
+ */
+
+ @Override
+ public boolean removeAllItems() throws UnsupportedOperationException {
+ if (autoCommit) {
+ /* Remove and commit instantly. */
+ try {
+ delegate.beginTransaction();
+ boolean success = true;
+ for (Object id : getItemIds()) {
+ if (!delegate.removeRow((RowItem) getItem(id))) {
+ success = false;
+ }
+ }
+ if (success) {
+ delegate.commit();
+ getLogger().log(Level.FINER, "All rows removed from DB...");
+ refresh();
+ if (notificationsEnabled) {
+ CacheFlushNotifier.notifyOfCacheFlush(this);
+ }
+ } else {
+ delegate.rollback();
+ }
+ return success;
+ } catch (SQLException e) {
+ getLogger().log(Level.WARNING,
+ "removeAllItems() failed, rolling back", e);
+ try {
+ delegate.rollback();
+ } catch (SQLException ee) {
+ /* Nothing can be done here */
+ getLogger().log(Level.SEVERE, "Failed to roll back", ee);
+ }
+ return false;
+ } catch (OptimisticLockException e) {
+ getLogger().log(Level.WARNING,
+ "removeAllItems() failed, rolling back", e);
+ try {
+ delegate.rollback();
+ } catch (SQLException ee) {
+ /* Nothing can be done here */
+ getLogger().log(Level.SEVERE, "Failed to roll back", ee);
+ }
+ throw e;
+ }
+ } else {
+ for (Object id : getItemIds()) {
+ removedItems.put((RowId) id, (RowItem) getItem(id));
+ cachedItems.remove(id);
+ }
+ refresh();
+ return true;
+ }
+ }
+
+ /*************************************************/
+ /** Methods from interface Container.Filterable **/
+ /*************************************************/
+
+ /**
+ * {@inheritDoc}
+ */
+
+ @Override
+ public void addContainerFilter(Filter filter)
+ throws UnsupportedFilterException {
+ // filter.setCaseSensitive(!ignoreCase);
+
+ filters.add(filter);
+ refresh();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+
+ @Override
+ public void removeContainerFilter(Filter filter) {
+ filters.remove(filter);
+ refresh();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public void addContainerFilter(Object propertyId, String filterString,
+ boolean ignoreCase, boolean onlyMatchPrefix) {
+ if (propertyId == null || !propertyIds.contains(propertyId)) {
+ return;
+ }
+
+ /* Generate Filter -object */
+ String likeStr = onlyMatchPrefix ? filterString + "%" : "%"
+ + filterString + "%";
+ Like like = new Like(propertyId.toString(), likeStr);
+ like.setCaseSensitive(!ignoreCase);
+ filters.add(like);
+ refresh();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public void removeContainerFilters(Object propertyId) {
+ ArrayList<Filter> toRemove = new ArrayList<Filter>();
+ for (Filter f : filters) {
+ if (f.appliesToProperty(propertyId)) {
+ toRemove.add(f);
+ }
+ }
+ filters.removeAll(toRemove);
+ refresh();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+
+ @Override
+ public void removeAllContainerFilters() {
+ filters.clear();
+ refresh();
+ }
+
+ /**********************************************/
+ /** Methods from interface Container.Indexed **/
+ /**********************************************/
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container.Indexed#indexOfId(java.lang.Object)
+ */
+
+ @Override
+ public int indexOfId(Object itemId) {
+ // First check if the id is in the added items
+ for (int ix = 0; ix < addedItems.size(); ix++) {
+ RowItem item = addedItems.get(ix);
+ if (item.getId().equals(itemId)) {
+ if (itemPassesFilters(item)) {
+ updateCount();
+ return size + ix;
+ } else {
+ return -1;
+ }
+ }
+ }
+
+ if (!containsId(itemId)) {
+ return -1;
+ }
+ if (cachedItems.isEmpty()) {
+ getPage();
+ }
+ int size = size();
+ boolean wrappedAround = false;
+ while (!wrappedAround) {
+ for (Integer i : itemIndexes.keySet()) {
+ if (itemIndexes.get(i).equals(itemId)) {
+ return i;
+ }
+ }
+ // load in the next page.
+ int nextIndex = (currentOffset / (pageLength * CACHE_RATIO) + 1)
+ * (pageLength * CACHE_RATIO);
+ if (nextIndex >= size) {
+ // Container wrapped around, start from index 0.
+ wrappedAround = true;
+ nextIndex = 0;
+ }
+ updateOffsetAndCache(nextIndex);
+ }
+ return -1;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container.Indexed#getIdByIndex(int)
+ */
+
+ @Override
+ public Object getIdByIndex(int index) {
+ if (index < 0 || index > size() - 1) {
+ return null;
+ }
+ if (index < size) {
+ if (itemIndexes.keySet().contains(index)) {
+ return itemIndexes.get(index);
+ }
+ updateOffsetAndCache(index);
+ return itemIndexes.get(index);
+ } else {
+ // The index is in the added items
+ int offset = index - size;
+ return addedItems.get(offset).getId();
+ }
+ }
+
+ /**********************************************/
+ /** Methods from interface Container.Ordered **/
+ /**********************************************/
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container.Ordered#nextItemId(java.lang.Object)
+ */
+
+ @Override
+ public Object nextItemId(Object itemId) {
+ return getIdByIndex(indexOfId(itemId) + 1);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container.Ordered#prevItemId(java.lang.Object)
+ */
+
+ @Override
+ public Object prevItemId(Object itemId) {
+ return getIdByIndex(indexOfId(itemId) - 1);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container.Ordered#firstItemId()
+ */
+
+ @Override
+ public Object firstItemId() {
+ updateCount();
+ if (size == 0) {
+ if (addedItems.isEmpty()) {
+ return null;
+ } else {
+ int ix = -1;
+ do {
+ ix++;
+ } while (!itemPassesFilters(addedItems.get(ix))
+ && ix < addedItems.size());
+ if (ix < addedItems.size()) {
+ return addedItems.get(ix).getId();
+ }
+ }
+ }
+ if (!itemIndexes.containsKey(0)) {
+ updateOffsetAndCache(0);
+ }
+ return itemIndexes.get(0);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container.Ordered#lastItemId()
+ */
+
+ @Override
+ public Object lastItemId() {
+ if (addedItems.isEmpty()) {
+ int lastIx = size() - 1;
+ if (!itemIndexes.containsKey(lastIx)) {
+ updateOffsetAndCache(size - 1);
+ }
+ return itemIndexes.get(lastIx);
+ } else {
+ int ix = addedItems.size();
+ do {
+ ix--;
+ } while (!itemPassesFilters(addedItems.get(ix)) && ix >= 0);
+ if (ix >= 0) {
+ return addedItems.get(ix).getId();
+ } else {
+ return null;
+ }
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container.Ordered#isFirstId(java.lang.Object)
+ */
+
+ @Override
+ public boolean isFirstId(Object itemId) {
+ return firstItemId().equals(itemId);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container.Ordered#isLastId(java.lang.Object)
+ */
+
+ @Override
+ public boolean isLastId(Object itemId) {
+ return lastItemId().equals(itemId);
+ }
+
+ /***********************************************/
+ /** Methods from interface Container.Sortable **/
+ /***********************************************/
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container.Sortable#sort(java.lang.Object[],
+ * boolean[])
+ */
+
+ @Override
+ public void sort(Object[] propertyId, boolean[] ascending) {
+ sorters.clear();
+ if (propertyId == null || propertyId.length == 0) {
+ refresh();
+ return;
+ }
+ /* Generate OrderBy -objects */
+ boolean asc = true;
+ for (int i = 0; i < propertyId.length; i++) {
+ /* Check that the property id is valid */
+ if (propertyId[i] instanceof String
+ && propertyIds.contains(propertyId[i])) {
+ try {
+ asc = ascending[i];
+ } catch (Exception e) {
+ getLogger().log(Level.WARNING, "", e);
+ }
+ sorters.add(new OrderBy((String) propertyId[i], asc));
+ }
+ }
+ refresh();
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container.Sortable#getSortableContainerPropertyIds()
+ */
+
+ @Override
+ public Collection<?> getSortableContainerPropertyIds() {
+ return getContainerPropertyIds();
+ }
+
+ /**************************************/
+ /** Methods specific to SQLContainer **/
+ /**************************************/
+
+ /**
+ * Refreshes the container - clears all caches and resets size and offset.
+ * Does NOT remove sorting or filtering rules!
+ */
+ public void refresh() {
+ sizeDirty = true;
+ currentOffset = 0;
+ cachedItems.clear();
+ itemIndexes.clear();
+ fireContentsChange();
+ }
+
+ /**
+ * Returns modify state of the container.
+ *
+ * @return true if contents of this container have been modified
+ */
+ public boolean isModified() {
+ return !removedItems.isEmpty() || !addedItems.isEmpty()
+ || !modifiedItems.isEmpty();
+ }
+
+ /**
+ * Set auto commit mode enabled or disabled. Auto commit mode means that all
+ * changes made to items of this container will be immediately written to
+ * the underlying data source.
+ *
+ * @param autoCommitEnabled
+ * true to enable auto commit mode
+ */
+ public void setAutoCommit(boolean autoCommitEnabled) {
+ autoCommit = autoCommitEnabled;
+ }
+
+ /**
+ * Returns status of the auto commit mode.
+ *
+ * @return true if auto commit mode is enabled
+ */
+ public boolean isAutoCommit() {
+ return autoCommit;
+ }
+
+ /**
+ * Returns the currently set page length.
+ *
+ * @return current page length
+ */
+ public int getPageLength() {
+ return pageLength;
+ }
+
+ /**
+ * Sets the page length used in lazy fetching of items from the data source.
+ * Also resets the cache size to match the new page length.
+ *
+ * As a side effect the container will be refreshed.
+ *
+ * @param pageLength
+ * new page length
+ */
+ public void setPageLength(int pageLength) {
+ setPageLengthInternal(pageLength);
+ refresh();
+ }
+
+ /**
+ * Sets the page length internally, without refreshing the container.
+ *
+ * @param pageLength
+ * the new page length
+ */
+ private void setPageLengthInternal(int pageLength) {
+ this.pageLength = pageLength > 0 ? pageLength : DEFAULT_PAGE_LENGTH;
+ cachedItems.setCacheLimit(CACHE_RATIO * getPageLength());
+ }
+
+ /**
+ * Adds the given OrderBy to this container and refreshes the container
+ * contents with the new sorting rules.
+ *
+ * Note that orderBy.getColumn() must return a column name that exists in
+ * this container.
+ *
+ * @param orderBy
+ * OrderBy to be added to the container sorting rules
+ */
+ public void addOrderBy(OrderBy orderBy) {
+ if (orderBy == null) {
+ return;
+ }
+ if (!propertyIds.contains(orderBy.getColumn())) {
+ throw new IllegalArgumentException(
+ "The column given for sorting does not exist in this container.");
+ }
+ sorters.add(orderBy);
+ refresh();
+ }
+
+ /**
+ * Commits all the changes, additions and removals made to the items of this
+ * container.
+ *
+ * @throws UnsupportedOperationException
+ * @throws SQLException
+ */
+ public void commit() throws UnsupportedOperationException, SQLException {
+ try {
+ getLogger().log(Level.FINER,
+ "Commiting changes through delegate...");
+ delegate.beginTransaction();
+ /* Perform buffered deletions */
+ for (RowItem item : removedItems.values()) {
+ if (!delegate.removeRow(item)) {
+ throw new SQLException("Removal failed for row with ID: "
+ + item.getId());
+ }
+ }
+ /* Perform buffered modifications */
+ for (RowItem item : modifiedItems) {
+ if (delegate.storeRow(item) > 0) {
+ /*
+ * Also reset the modified state in the item in case it is
+ * reused e.g. in a form.
+ */
+ item.commit();
+ } else {
+ delegate.rollback();
+ refresh();
+ throw new ConcurrentModificationException(
+ "Item with the ID '" + item.getId()
+ + "' has been externally modified.");
+ }
+ }
+ /* Perform buffered additions */
+ for (RowItem item : addedItems) {
+ delegate.storeRow(item);
+ }
+ delegate.commit();
+ removedItems.clear();
+ addedItems.clear();
+ modifiedItems.clear();
+ refresh();
+ if (notificationsEnabled) {
+ CacheFlushNotifier.notifyOfCacheFlush(this);
+ }
+ } catch (SQLException e) {
+ delegate.rollback();
+ throw e;
+ } catch (OptimisticLockException e) {
+ delegate.rollback();
+ throw e;
+ }
+ }
+
+ /**
+ * Rolls back all the changes, additions and removals made to the items of
+ * this container.
+ *
+ * @throws UnsupportedOperationException
+ * @throws SQLException
+ */
+ public void rollback() throws UnsupportedOperationException, SQLException {
+ getLogger().log(Level.FINE, "Rolling back changes...");
+ removedItems.clear();
+ addedItems.clear();
+ modifiedItems.clear();
+ refresh();
+ }
+
+ /**
+ * Notifies this container that a property in the given item has been
+ * modified. The change will be buffered or made instantaneously depending
+ * on auto commit mode.
+ *
+ * @param changedItem
+ * item that has a modified property
+ */
+ void itemChangeNotification(RowItem changedItem) {
+ if (autoCommit) {
+ try {
+ delegate.beginTransaction();
+ if (delegate.storeRow(changedItem) == 0) {
+ delegate.rollback();
+ refresh();
+ throw new ConcurrentModificationException(
+ "Item with the ID '" + changedItem.getId()
+ + "' has been externally modified.");
+ }
+ delegate.commit();
+ if (notificationsEnabled) {
+ CacheFlushNotifier.notifyOfCacheFlush(this);
+ }
+ getLogger().log(Level.FINER, "Row updated to DB...");
+ } catch (SQLException e) {
+ getLogger().log(Level.WARNING,
+ "itemChangeNotification failed, rolling back...", e);
+ try {
+ delegate.rollback();
+ } catch (SQLException ee) {
+ /* Nothing can be done here */
+ getLogger().log(Level.SEVERE, "Rollback failed", e);
+ }
+ throw new RuntimeException(e);
+ }
+ } else {
+ if (!(changedItem.getId() instanceof TemporaryRowId)
+ && !modifiedItems.contains(changedItem)) {
+ modifiedItems.add(changedItem);
+ }
+ }
+ }
+
+ /**
+ * Determines a new offset for updating the row cache. The offset is
+ * calculated from the given index, and will be fixed to match the start of
+ * a page, based on the value of pageLength.
+ *
+ * @param index
+ * Index of the item that was requested, but not found in cache
+ */
+ private void updateOffsetAndCache(int index) {
+ if (itemIndexes.containsKey(index)) {
+ return;
+ }
+ currentOffset = (index / (pageLength * CACHE_RATIO))
+ * (pageLength * CACHE_RATIO);
+ if (currentOffset < 0) {
+ currentOffset = 0;
+ }
+ getPage();
+ }
+
+ /**
+ * Fetches new count of rows from the data source, if needed.
+ */
+ private void updateCount() {
+ if (!sizeDirty
+ && new Date().getTime() < sizeUpdated.getTime()
+ + sizeValidMilliSeconds) {
+ return;
+ }
+ try {
+ try {
+ delegate.setFilters(filters);
+ } catch (UnsupportedOperationException e) {
+ getLogger().log(Level.FINE,
+ "The query delegate doesn't support filtering", e);
+ }
+ try {
+ delegate.setOrderBy(sorters);
+ } catch (UnsupportedOperationException e) {
+ getLogger().log(Level.FINE,
+ "The query delegate doesn't support filtering", e);
+ }
+ int newSize = delegate.getCount();
+ if (newSize != size) {
+ size = newSize;
+ refresh();
+ }
+ sizeUpdated = new Date();
+ sizeDirty = false;
+ getLogger().log(Level.FINER,
+ "Updated row count. New count is: " + size);
+ } catch (SQLException e) {
+ throw new RuntimeException("Failed to update item set size.", e);
+ }
+ }
+
+ /**
+ * Fetches property id's (column names and their types) from the data
+ * source.
+ *
+ * @throws SQLException
+ */
+ private void getPropertyIds() throws SQLException {
+ propertyIds.clear();
+ propertyTypes.clear();
+ delegate.setFilters(null);
+ delegate.setOrderBy(null);
+ ResultSet rs = null;
+ ResultSetMetaData rsmd = null;
+ try {
+ delegate.beginTransaction();
+ rs = delegate.getResults(0, 1);
+ boolean resultExists = rs.next();
+ rsmd = rs.getMetaData();
+ Class<?> type = null;
+ for (int i = 1; i <= rsmd.getColumnCount(); i++) {
+ if (!isColumnIdentifierValid(rsmd.getColumnLabel(i))) {
+ continue;
+ }
+ String colName = rsmd.getColumnLabel(i);
+ /*
+ * Make sure not to add the same colName twice. This can easily
+ * happen if the SQL query joins many tables with an ID column.
+ */
+ if (!propertyIds.contains(colName)) {
+ propertyIds.add(colName);
+ }
+ /* Try to determine the column's JDBC class by all means. */
+ if (resultExists && rs.getObject(i) != null) {
+ type = rs.getObject(i).getClass();
+ } else {
+ try {
+ type = Class.forName(rsmd.getColumnClassName(i));
+ } catch (Exception e) {
+ getLogger().log(Level.WARNING, "Class not found", e);
+ /* On failure revert to Object and hope for the best. */
+ type = Object.class;
+ }
+ }
+ /*
+ * Determine read only and nullability status of the column. A
+ * column is read only if it is reported as either read only or
+ * auto increment by the database, and also it is set as the
+ * version column in a TableQuery delegate.
+ */
+ boolean readOnly = rsmd.isAutoIncrement(i)
+ || rsmd.isReadOnly(i);
+ if (delegate instanceof TableQuery
+ && rsmd.getColumnLabel(i).equals(
+ ((TableQuery) delegate).getVersionColumn())) {
+ readOnly = true;
+ }
+ propertyReadOnly.put(colName, readOnly);
+ propertyNullable.put(colName,
+ rsmd.isNullable(i) == ResultSetMetaData.columnNullable);
+ propertyTypes.put(colName, type);
+ }
+ rs.getStatement().close();
+ rs.close();
+ delegate.commit();
+ getLogger().log(Level.FINER, "Property IDs fetched.");
+ } catch (SQLException e) {
+ getLogger().log(Level.WARNING,
+ "Failed to fetch property ids, rolling back", e);
+ try {
+ delegate.rollback();
+ } catch (SQLException e1) {
+ getLogger().log(Level.SEVERE, "Failed to roll back", e1);
+ }
+ try {
+ if (rs != null) {
+ if (rs.getStatement() != null) {
+ rs.getStatement().close();
+ }
+ rs.close();
+ }
+ } catch (SQLException e1) {
+ getLogger().log(Level.WARNING, "Failed to close session", e1);
+ }
+ throw e;
+ }
+ }
+
+ /**
+ * Fetches a page from the data source based on the values of pageLenght and
+ * currentOffset. Also updates the set of primary keys, used in
+ * identification of RowItems.
+ */
+ private void getPage() {
+ updateCount();
+ ResultSet rs = null;
+ ResultSetMetaData rsmd = null;
+ cachedItems.clear();
+ itemIndexes.clear();
+ try {
+ try {
+ delegate.setOrderBy(sorters);
+ } catch (UnsupportedOperationException e) {
+ /* The query delegate doesn't support sorting. */
+ /* No need to do anything. */
+ getLogger().log(Level.FINE,
+ "The query delegate doesn't support sorting", e);
+ }
+ delegate.beginTransaction();
+ rs = delegate.getResults(currentOffset, pageLength * CACHE_RATIO);
+ rsmd = rs.getMetaData();
+ List<String> pKeys = delegate.getPrimaryKeyColumns();
+ // }
+ /* Create new items and column properties */
+ ColumnProperty cp = null;
+ int rowCount = currentOffset;
+ if (!delegate.implementationRespectsPagingLimits()) {
+ rowCount = currentOffset = 0;
+ setPageLengthInternal(size);
+ }
+ while (rs.next()) {
+ List<ColumnProperty> itemProperties = new ArrayList<ColumnProperty>();
+ /* Generate row itemId based on primary key(s) */
+ Object[] itemId = new Object[pKeys.size()];
+ for (int i = 0; i < pKeys.size(); i++) {
+ itemId[i] = rs.getObject(pKeys.get(i));
+ }
+ RowId id = null;
+ if (pKeys.isEmpty()) {
+ id = new ReadOnlyRowId(rs.getRow());
+ } else {
+ id = new RowId(itemId);
+ }
+ List<String> propertiesToAdd = new ArrayList<String>(
+ propertyIds);
+ if (!removedItems.containsKey(id)) {
+ for (int i = 1; i <= rsmd.getColumnCount(); i++) {
+ if (!isColumnIdentifierValid(rsmd.getColumnLabel(i))) {
+ continue;
+ }
+ String colName = rsmd.getColumnLabel(i);
+ Object value = rs.getObject(i);
+ Class<?> type = value != null ? value.getClass()
+ : Object.class;
+ if (value == null) {
+ for (String propName : propertyTypes.keySet()) {
+ if (propName.equals(rsmd.getColumnLabel(i))) {
+ type = propertyTypes.get(propName);
+ break;
+ }
+ }
+ }
+ /*
+ * In case there are more than one column with the same
+ * name, add only the first one. This can easily happen
+ * if you join many tables where each table has an ID
+ * column.
+ */
+ if (propertiesToAdd.contains(colName)) {
+ cp = new ColumnProperty(colName,
+ propertyReadOnly.get(colName),
+ !propertyReadOnly.get(colName),
+ propertyNullable.get(colName), value, type);
+ itemProperties.add(cp);
+ propertiesToAdd.remove(colName);
+ }
+ }
+ /* Cache item */
+ itemIndexes.put(rowCount, id);
+
+ // if an item with the id is contained in the modified
+ // cache, then use this record and add it to the cached
+ // items. Otherwise create a new item
+ int modifiedIndex = indexInModifiedCache(id);
+ if (modifiedIndex != -1) {
+ cachedItems.put(id, modifiedItems.get(modifiedIndex));
+ } else {
+ cachedItems.put(id, new RowItem(this, id,
+ itemProperties));
+ }
+
+ rowCount++;
+ }
+ }
+ rs.getStatement().close();
+ rs.close();
+ delegate.commit();
+ getLogger().log(
+ Level.FINER,
+ "Fetched " + pageLength * CACHE_RATIO
+ + " rows starting from " + currentOffset);
+ } catch (SQLException e) {
+ getLogger().log(Level.WARNING,
+ "Failed to fetch rows, rolling back", e);
+ try {
+ delegate.rollback();
+ } catch (SQLException e1) {
+ getLogger().log(Level.SEVERE, "Failed to roll back", e1);
+ }
+ try {
+ if (rs != null) {
+ if (rs.getStatement() != null) {
+ rs.getStatement().close();
+ rs.close();
+ }
+ }
+ } catch (SQLException e1) {
+ getLogger().log(Level.WARNING, "Failed to close session", e1);
+ }
+ throw new RuntimeException("Failed to fetch page.", e);
+ }
+ }
+
+ /**
+ * Returns the index of the item with the given itemId for the modified
+ * cache.
+ *
+ * @param itemId
+ * @return the index of the item with the itemId in the modified cache. Or
+ * -1 if not found.
+ */
+ private int indexInModifiedCache(Object itemId) {
+ for (int ix = 0; ix < modifiedItems.size(); ix++) {
+ RowItem item = modifiedItems.get(ix);
+ if (item.getId().equals(itemId)) {
+ return ix;
+ }
+ }
+ return -1;
+ }
+
+ private int sizeOfAddedItems() {
+ return getFilteredAddedItems().size();
+ }
+
+ private List<RowItem> getFilteredAddedItems() {
+ ArrayList<RowItem> filtered = new ArrayList<RowItem>(addedItems);
+ if (filters != null && !filters.isEmpty()) {
+ for (RowItem item : addedItems) {
+ if (!itemPassesFilters(item)) {
+ filtered.remove(item);
+ }
+ }
+ }
+ return filtered;
+ }
+
+ private boolean itemPassesFilters(RowItem item) {
+ for (Filter filter : filters) {
+ if (!filter.passesFilter(item.getId(), item)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Checks is the given column identifier valid to be used with SQLContainer.
+ * Currently the only non-valid identifier is "rownum" when MSSQL or Oracle
+ * is used. This is due to the way the SELECT queries are constructed in
+ * order to implement paging in these databases.
+ *
+ * @param identifier
+ * Column identifier
+ * @return true if the identifier is valid
+ */
+ private boolean isColumnIdentifierValid(String identifier) {
+ if (identifier.equalsIgnoreCase("rownum")
+ && delegate instanceof TableQuery) {
+ TableQuery tq = (TableQuery) delegate;
+ if (tq.getSqlGenerator() instanceof MSSQLGenerator
+ || tq.getSqlGenerator() instanceof OracleGenerator) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Returns the QueryDelegate set for this SQLContainer.
+ *
+ * @return current querydelegate
+ */
+ protected QueryDelegate getQueryDelegate() {
+ return delegate;
+ }
+
+ /************************************/
+ /** UNSUPPORTED CONTAINER FEATURES **/
+ /************************************/
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container#addContainerProperty(java.lang.Object,
+ * java.lang.Class, java.lang.Object)
+ */
+
+ @Override
+ public boolean addContainerProperty(Object propertyId, Class<?> type,
+ Object defaultValue) throws UnsupportedOperationException {
+ throw new UnsupportedOperationException();
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container#removeContainerProperty(java.lang.Object)
+ */
+
+ @Override
+ public boolean removeContainerProperty(Object propertyId)
+ throws UnsupportedOperationException {
+ throw new UnsupportedOperationException();
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container#addItem(java.lang.Object)
+ */
+
+ @Override
+ public Item addItem(Object itemId) throws UnsupportedOperationException {
+ throw new UnsupportedOperationException();
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container.Ordered#addItemAfter(java.lang.Object,
+ * java.lang.Object)
+ */
+
+ @Override
+ public Item addItemAfter(Object previousItemId, Object newItemId)
+ throws UnsupportedOperationException {
+ throw new UnsupportedOperationException();
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container.Indexed#addItemAt(int, java.lang.Object)
+ */
+
+ @Override
+ public Item addItemAt(int index, Object newItemId)
+ throws UnsupportedOperationException {
+ throw new UnsupportedOperationException();
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container.Indexed#addItemAt(int)
+ */
+
+ @Override
+ public Object addItemAt(int index) throws UnsupportedOperationException {
+ throw new UnsupportedOperationException();
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container.Ordered#addItemAfter(java.lang.Object)
+ */
+
+ @Override
+ public Object addItemAfter(Object previousItemId)
+ throws UnsupportedOperationException {
+ throw new UnsupportedOperationException();
+ }
+
+ /******************************************/
+ /** ITEMSETCHANGENOTIFIER IMPLEMENTATION **/
+ /******************************************/
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.data.Container.ItemSetChangeNotifier#addListener(com.vaadin
+ * .data.Container.ItemSetChangeListener)
+ */
+
+ @Override
+ public void addListener(Container.ItemSetChangeListener listener) {
+ if (itemSetChangeListeners == null) {
+ itemSetChangeListeners = new LinkedList<Container.ItemSetChangeListener>();
+ }
+ itemSetChangeListeners.add(listener);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.data.Container.ItemSetChangeNotifier#removeListener(com.vaadin
+ * .data.Container.ItemSetChangeListener)
+ */
+
+ @Override
+ public void removeListener(Container.ItemSetChangeListener listener) {
+ if (itemSetChangeListeners != null) {
+ itemSetChangeListeners.remove(listener);
+ }
+ }
+
+ protected void fireContentsChange() {
+ if (itemSetChangeListeners != null) {
+ final Object[] l = itemSetChangeListeners.toArray();
+ final Container.ItemSetChangeEvent event = new SQLContainer.ItemSetChangeEvent(
+ this);
+ for (int i = 0; i < l.length; i++) {
+ ((Container.ItemSetChangeListener) l[i])
+ .containerItemSetChange(event);
+ }
+ }
+ }
+
+ /**
+ * Simple ItemSetChangeEvent implementation.
+ */
+ @SuppressWarnings("serial")
+ public static class ItemSetChangeEvent extends EventObject implements
+ Container.ItemSetChangeEvent {
+
+ private ItemSetChangeEvent(SQLContainer source) {
+ super(source);
+ }
+
+ @Override
+ public Container getContainer() {
+ return (Container) getSource();
+ }
+ }
+
+ /**************************************************/
+ /** ROWIDCHANGELISTENER PASSING TO QUERYDELEGATE **/
+ /**************************************************/
+
+ /**
+ * Adds a RowIdChangeListener to the QueryDelegate
+ *
+ * @param listener
+ */
+ public void addListener(RowIdChangeListener listener) {
+ if (delegate instanceof QueryDelegate.RowIdChangeNotifier) {
+ ((QueryDelegate.RowIdChangeNotifier) delegate)
+ .addListener(listener);
+ }
+ }
+
+ /**
+ * Removes a RowIdChangeListener from the QueryDelegate
+ *
+ * @param listener
+ */
+ public void removeListener(RowIdChangeListener listener) {
+ if (delegate instanceof QueryDelegate.RowIdChangeNotifier) {
+ ((QueryDelegate.RowIdChangeNotifier) delegate)
+ .removeListener(listener);
+ }
+ }
+
+ /**
+ * Calling this will enable this SQLContainer to send and receive cache
+ * flush notifications for its lifetime.
+ */
+ public void enableCacheFlushNotifications() {
+ if (!notificationsEnabled) {
+ notificationsEnabled = true;
+ CacheFlushNotifier.addInstance(this);
+ }
+ }
+
+ /******************************************/
+ /** Referencing mechanism implementation **/
+ /******************************************/
+
+ /**
+ * Adds a new reference to the given SQLContainer. In addition to the
+ * container you must provide the column (property) names used for the
+ * reference in both this and the referenced SQLContainer.
+ *
+ * Note that multiple references pointing to the same SQLContainer are not
+ * supported.
+ *
+ * @param refdCont
+ * Target SQLContainer of the new reference
+ * @param refingCol
+ * Column (property) name in this container storing the (foreign
+ * key) reference
+ * @param refdCol
+ * Column (property) name in the referenced container storing the
+ * referenced key
+ */
+ public void addReference(SQLContainer refdCont, String refingCol,
+ String refdCol) {
+ if (refdCont == null) {
+ throw new IllegalArgumentException(
+ "Referenced SQLContainer can not be null.");
+ }
+ if (!getContainerPropertyIds().contains(refingCol)) {
+ throw new IllegalArgumentException(
+ "Given referencing column name is invalid."
+ + " Please ensure that this container"
+ + " contains a property ID named: " + refingCol);
+ }
+ if (!refdCont.getContainerPropertyIds().contains(refdCol)) {
+ throw new IllegalArgumentException(
+ "Given referenced column name is invalid."
+ + " Please ensure that the referenced container"
+ + " contains a property ID named: " + refdCol);
+ }
+ if (references.keySet().contains(refdCont)) {
+ throw new IllegalArgumentException(
+ "An SQLContainer instance can only be referenced once.");
+ }
+ references.put(refdCont, new Reference(refdCont, refingCol, refdCol));
+ }
+
+ /**
+ * Removes the reference pointing to the given SQLContainer.
+ *
+ * @param refdCont
+ * Target SQLContainer of the reference
+ * @return true if successful, false if the reference did not exist
+ */
+ public boolean removeReference(SQLContainer refdCont) {
+ if (refdCont == null) {
+ throw new IllegalArgumentException(
+ "Referenced SQLContainer can not be null.");
+ }
+ return references.remove(refdCont) == null ? false : true;
+ }
+
+ /**
+ * Sets the referenced item. The referencing column of the item in this
+ * container is updated accordingly.
+ *
+ * @param itemId
+ * Item Id of the reference source (from this container)
+ * @param refdItemId
+ * Item Id of the reference target (from referenced container)
+ * @param refdCont
+ * Target SQLContainer of the reference
+ * @return true if the referenced item was successfully set, false on
+ * failure
+ */
+ public boolean setReferencedItem(Object itemId, Object refdItemId,
+ SQLContainer refdCont) {
+ if (refdCont == null) {
+ throw new IllegalArgumentException(
+ "Referenced SQLContainer can not be null.");
+ }
+ Reference r = references.get(refdCont);
+ if (r == null) {
+ throw new IllegalArgumentException(
+ "Reference to the given SQLContainer not defined.");
+ }
+ try {
+ getContainerProperty(itemId, r.getReferencingColumn()).setValue(
+ refdCont.getContainerProperty(refdItemId,
+ r.getReferencedColumn()));
+ return true;
+ } catch (Exception e) {
+ getLogger()
+ .log(Level.WARNING, "Setting referenced item failed.", e);
+ return false;
+ }
+ }
+
+ /**
+ * Fetches the Item Id of the referenced item from the target SQLContainer.
+ *
+ * @param itemId
+ * Item Id of the reference source (from this container)
+ * @param refdCont
+ * Target SQLContainer of the reference
+ * @return Item Id of the referenced item, or null if not found
+ */
+ public Object getReferencedItemId(Object itemId, SQLContainer refdCont) {
+ if (refdCont == null) {
+ throw new IllegalArgumentException(
+ "Referenced SQLContainer can not be null.");
+ }
+ Reference r = references.get(refdCont);
+ if (r == null) {
+ throw new IllegalArgumentException(
+ "Reference to the given SQLContainer not defined.");
+ }
+ Object refKey = getContainerProperty(itemId, r.getReferencingColumn())
+ .getValue();
+
+ refdCont.removeAllContainerFilters();
+ refdCont.addContainerFilter(new Equal(r.getReferencedColumn(), refKey));
+ Object toReturn = refdCont.firstItemId();
+ refdCont.removeAllContainerFilters();
+ return toReturn;
+ }
+
+ /**
+ * Fetches the referenced item from the target SQLContainer.
+ *
+ * @param itemId
+ * Item Id of the reference source (from this container)
+ * @param refdCont
+ * Target SQLContainer of the reference
+ * @return The referenced item, or null if not found
+ */
+ public Item getReferencedItem(Object itemId, SQLContainer refdCont) {
+ return refdCont.getItem(getReferencedItemId(itemId, refdCont));
+ }
+
+ private void writeObject(java.io.ObjectOutputStream out) throws IOException {
+ out.defaultWriteObject();
+ }
+
+ private void readObject(java.io.ObjectInputStream in) throws IOException,
+ ClassNotFoundException {
+ in.defaultReadObject();
+ if (notificationsEnabled) {
+ /*
+ * Register instance with CacheFlushNotifier after de-serialization
+ * if notifications are enabled
+ */
+ CacheFlushNotifier.addInstance(this);
+ }
+ }
+
+ private static final Logger getLogger() {
+ return Logger.getLogger(SQLContainer.class.getName());
+ }
+}
diff --git a/server/src/com/vaadin/data/util/sqlcontainer/SQLUtil.java b/server/src/com/vaadin/data/util/sqlcontainer/SQLUtil.java
new file mode 100644
index 0000000000..4a48dbf499
--- /dev/null
+++ b/server/src/com/vaadin/data/util/sqlcontainer/SQLUtil.java
@@ -0,0 +1,36 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util.sqlcontainer;
+
+import java.io.Serializable;
+
+public class SQLUtil implements Serializable {
+ /**
+ * Escapes different special characters in strings that are passed to SQL.
+ * Replaces the following:
+ *
+ * <list> <li>' is replaced with ''</li> <li>\x00 is removed</li> <li>\ is
+ * replaced with \\</li> <li>" is replaced with \"</li> <li>
+ * \x1a is removed</li> </list>
+ *
+ * Also note! The escaping done here may or may not be enough to prevent any
+ * and all SQL injections so it is recommended to check user input before
+ * giving it to the SQLContainer/TableQuery.
+ *
+ * @param constant
+ * @return \\\'\'
+ */
+ public static String escapeSQL(String constant) {
+ if (constant == null) {
+ return null;
+ }
+ String fixedConstant = constant;
+ fixedConstant = fixedConstant.replaceAll("\\\\x00", "");
+ fixedConstant = fixedConstant.replaceAll("\\\\x1a", "");
+ fixedConstant = fixedConstant.replaceAll("'", "''");
+ fixedConstant = fixedConstant.replaceAll("\\\\", "\\\\\\\\");
+ fixedConstant = fixedConstant.replaceAll("\\\"", "\\\\\"");
+ return fixedConstant;
+ }
+}
diff --git a/server/src/com/vaadin/data/util/sqlcontainer/TemporaryRowId.java b/server/src/com/vaadin/data/util/sqlcontainer/TemporaryRowId.java
new file mode 100644
index 0000000000..b4bca75a2a
--- /dev/null
+++ b/server/src/com/vaadin/data/util/sqlcontainer/TemporaryRowId.java
@@ -0,0 +1,32 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util.sqlcontainer;
+
+public class TemporaryRowId extends RowId {
+ private static final long serialVersionUID = -641983830469018329L;
+
+ public TemporaryRowId(Object[] id) {
+ super(id);
+ }
+
+ @Override
+ public int hashCode() {
+ return id.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == null || !(obj instanceof TemporaryRowId)) {
+ return false;
+ }
+ Object[] compId = ((TemporaryRowId) obj).getId();
+ return id.equals(compId);
+ }
+
+ @Override
+ public String toString() {
+ return "Temporary row id";
+ }
+
+}
diff --git a/server/src/com/vaadin/data/util/sqlcontainer/connection/J2EEConnectionPool.java b/server/src/com/vaadin/data/util/sqlcontainer/connection/J2EEConnectionPool.java
new file mode 100644
index 0000000000..9aa4f7c4be
--- /dev/null
+++ b/server/src/com/vaadin/data/util/sqlcontainer/connection/J2EEConnectionPool.java
@@ -0,0 +1,72 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util.sqlcontainer.connection;
+
+import java.sql.Connection;
+import java.sql.SQLException;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import javax.naming.InitialContext;
+import javax.naming.NamingException;
+import javax.sql.DataSource;
+
+public class J2EEConnectionPool implements JDBCConnectionPool {
+
+ private String dataSourceJndiName;
+
+ private DataSource dataSource = null;
+
+ public J2EEConnectionPool(DataSource dataSource) {
+ this.dataSource = dataSource;
+ }
+
+ public J2EEConnectionPool(String dataSourceJndiName) {
+ this.dataSourceJndiName = dataSourceJndiName;
+ }
+
+ @Override
+ public Connection reserveConnection() throws SQLException {
+ Connection conn = getDataSource().getConnection();
+ conn.setAutoCommit(false);
+
+ return conn;
+ }
+
+ private DataSource getDataSource() throws SQLException {
+ if (dataSource == null) {
+ dataSource = lookupDataSource();
+ }
+ return dataSource;
+ }
+
+ private DataSource lookupDataSource() throws SQLException {
+ try {
+ InitialContext ic = new InitialContext();
+ return (DataSource) ic.lookup(dataSourceJndiName);
+ } catch (NamingException e) {
+ throw new SQLException(
+ "NamingException - Cannot connect to the database. Cause: "
+ + e.getMessage());
+ }
+ }
+
+ @Override
+ public void releaseConnection(Connection conn) {
+ if (conn != null) {
+ try {
+ conn.close();
+ } catch (SQLException e) {
+ Logger.getLogger(J2EEConnectionPool.class.getName()).log(
+ Level.FINE, "Could not release SQL connection", e);
+ }
+ }
+ }
+
+ @Override
+ public void destroy() {
+ dataSource = null;
+ }
+
+} \ No newline at end of file
diff --git a/server/src/com/vaadin/data/util/sqlcontainer/connection/JDBCConnectionPool.java b/server/src/com/vaadin/data/util/sqlcontainer/connection/JDBCConnectionPool.java
new file mode 100644
index 0000000000..cf12461588
--- /dev/null
+++ b/server/src/com/vaadin/data/util/sqlcontainer/connection/JDBCConnectionPool.java
@@ -0,0 +1,41 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util.sqlcontainer.connection;
+
+import java.io.Serializable;
+import java.sql.Connection;
+import java.sql.SQLException;
+
+/**
+ * Interface for implementing connection pools to be used with SQLContainer.
+ */
+public interface JDBCConnectionPool extends Serializable {
+ /**
+ * Retrieves a connection.
+ *
+ * @return a usable connection to the database
+ * @throws SQLException
+ */
+ public Connection reserveConnection() throws SQLException;
+
+ /**
+ * Releases a connection that was retrieved earlier.
+ *
+ * Note that depending on implementation, the transaction possibly open in
+ * the connection may or may not be rolled back.
+ *
+ * @param conn
+ * Connection to be released
+ */
+ public void releaseConnection(Connection conn);
+
+ /**
+ * Destroys the connection pool: close() is called an all the connections in
+ * the pool, whether available or reserved.
+ *
+ * This method was added to fix PostgreSQL -related issues with connections
+ * that were left hanging 'idle'.
+ */
+ public void destroy();
+}
diff --git a/server/src/com/vaadin/data/util/sqlcontainer/connection/SimpleJDBCConnectionPool.java b/server/src/com/vaadin/data/util/sqlcontainer/connection/SimpleJDBCConnectionPool.java
new file mode 100644
index 0000000000..21760014b9
--- /dev/null
+++ b/server/src/com/vaadin/data/util/sqlcontainer/connection/SimpleJDBCConnectionPool.java
@@ -0,0 +1,168 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util.sqlcontainer.connection;
+
+import java.io.IOException;
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Simple implementation of the JDBCConnectionPool interface. Handles loading
+ * the JDBC driver, setting up the connections and ensuring they are still
+ * usable upon release.
+ */
+@SuppressWarnings("serial")
+public class SimpleJDBCConnectionPool implements JDBCConnectionPool {
+
+ private int initialConnections = 5;
+ private int maxConnections = 20;
+
+ private String driverName;
+ private String connectionUri;
+ private String userName;
+ private String password;
+
+ private transient Set<Connection> availableConnections;
+ private transient Set<Connection> reservedConnections;
+
+ private boolean initialized;
+
+ public SimpleJDBCConnectionPool(String driverName, String connectionUri,
+ String userName, String password) throws SQLException {
+ if (driverName == null) {
+ throw new IllegalArgumentException(
+ "JDBC driver class name must be given.");
+ }
+ if (connectionUri == null) {
+ throw new IllegalArgumentException(
+ "Database connection URI must be given.");
+ }
+ if (userName == null) {
+ throw new IllegalArgumentException(
+ "Database username must be given.");
+ }
+ if (password == null) {
+ throw new IllegalArgumentException(
+ "Database password must be given.");
+ }
+ this.driverName = driverName;
+ this.connectionUri = connectionUri;
+ this.userName = userName;
+ this.password = password;
+
+ /* Initialize JDBC driver */
+ try {
+ Class.forName(driverName).newInstance();
+ } catch (Exception ex) {
+ throw new RuntimeException("Specified JDBC Driver: " + driverName
+ + " - initialization failed.", ex);
+ }
+ }
+
+ public SimpleJDBCConnectionPool(String driverName, String connectionUri,
+ String userName, String password, int initialConnections,
+ int maxConnections) throws SQLException {
+ this(driverName, connectionUri, userName, password);
+ this.initialConnections = initialConnections;
+ this.maxConnections = maxConnections;
+ }
+
+ private void initializeConnections() throws SQLException {
+ availableConnections = new HashSet<Connection>(initialConnections);
+ reservedConnections = new HashSet<Connection>(initialConnections);
+ for (int i = 0; i < initialConnections; i++) {
+ availableConnections.add(createConnection());
+ }
+ initialized = true;
+ }
+
+ @Override
+ public synchronized Connection reserveConnection() throws SQLException {
+ if (!initialized) {
+ initializeConnections();
+ }
+ if (availableConnections.isEmpty()) {
+ if (reservedConnections.size() < maxConnections) {
+ availableConnections.add(createConnection());
+ } else {
+ throw new SQLException("Connection limit has been reached.");
+ }
+ }
+
+ Connection c = availableConnections.iterator().next();
+ availableConnections.remove(c);
+ reservedConnections.add(c);
+
+ return c;
+ }
+
+ @Override
+ public synchronized void releaseConnection(Connection conn) {
+ if (conn == null || !initialized) {
+ return;
+ }
+ /* Try to roll back if necessary */
+ try {
+ if (!conn.getAutoCommit()) {
+ conn.rollback();
+ }
+ } catch (SQLException e) {
+ /* Roll back failed, close and discard connection */
+ try {
+ conn.close();
+ } catch (SQLException e1) {
+ /* Nothing needs to be done */
+ }
+ reservedConnections.remove(conn);
+ return;
+ }
+ reservedConnections.remove(conn);
+ availableConnections.add(conn);
+ }
+
+ private Connection createConnection() throws SQLException {
+ Connection c = DriverManager.getConnection(connectionUri, userName,
+ password);
+ c.setAutoCommit(false);
+ if (driverName.toLowerCase().contains("mysql")) {
+ try {
+ Statement s = c.createStatement();
+ s.execute("SET SESSION sql_mode = 'ANSI'");
+ s.close();
+ } catch (Exception e) {
+ // Failed to set ansi mode; continue
+ }
+ }
+ return c;
+ }
+
+ @Override
+ public void destroy() {
+ for (Connection c : availableConnections) {
+ try {
+ c.close();
+ } catch (SQLException e) {
+ // No need to do anything
+ }
+ }
+ for (Connection c : reservedConnections) {
+ try {
+ c.close();
+ } catch (SQLException e) {
+ // No need to do anything
+ }
+ }
+
+ }
+
+ private void writeObject(java.io.ObjectOutputStream out) throws IOException {
+ initialized = false;
+ out.defaultWriteObject();
+ }
+
+}
diff --git a/server/src/com/vaadin/data/util/sqlcontainer/query/FreeformQuery.java b/server/src/com/vaadin/data/util/sqlcontainer/query/FreeformQuery.java
new file mode 100644
index 0000000000..ec986fab95
--- /dev/null
+++ b/server/src/com/vaadin/data/util/sqlcontainer/query/FreeformQuery.java
@@ -0,0 +1,507 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util.sqlcontainer.query;
+
+import java.io.IOException;
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import com.vaadin.data.Container.Filter;
+import com.vaadin.data.util.sqlcontainer.RowItem;
+import com.vaadin.data.util.sqlcontainer.SQLContainer;
+import com.vaadin.data.util.sqlcontainer.connection.JDBCConnectionPool;
+import com.vaadin.data.util.sqlcontainer.query.generator.StatementHelper;
+import com.vaadin.data.util.sqlcontainer.query.generator.filter.QueryBuilder;
+
+@SuppressWarnings("serial")
+public class FreeformQuery implements QueryDelegate {
+
+ FreeformQueryDelegate delegate = null;
+ private String queryString;
+ private List<String> primaryKeyColumns;
+ private JDBCConnectionPool connectionPool;
+ private transient Connection activeConnection = null;
+
+ /**
+ * Prevent no-parameters instantiation of FreeformQuery
+ */
+ @SuppressWarnings("unused")
+ private FreeformQuery() {
+ }
+
+ /**
+ * Creates a new freeform query delegate to be used with the
+ * {@link SQLContainer}.
+ *
+ * @param queryString
+ * The actual query to perform.
+ * @param primaryKeyColumns
+ * The primary key columns. Read-only mode is forced if this
+ * parameter is null or empty.
+ * @param connectionPool
+ * the JDBCConnectionPool to use to open connections to the SQL
+ * database.
+ * @deprecated @see
+ * {@link FreeformQuery#FreeformQuery(String, JDBCConnectionPool, String...)}
+ */
+ @Deprecated
+ public FreeformQuery(String queryString, List<String> primaryKeyColumns,
+ JDBCConnectionPool connectionPool) {
+ if (primaryKeyColumns == null) {
+ primaryKeyColumns = new ArrayList<String>();
+ }
+ if (primaryKeyColumns.contains("")) {
+ throw new IllegalArgumentException(
+ "The primary key columns contain an empty string!");
+ } else if (queryString == null || "".equals(queryString)) {
+ throw new IllegalArgumentException(
+ "The query string may not be empty or null!");
+ } else if (connectionPool == null) {
+ throw new IllegalArgumentException(
+ "The connectionPool may not be null!");
+ }
+ this.queryString = queryString;
+ this.primaryKeyColumns = Collections
+ .unmodifiableList(primaryKeyColumns);
+ this.connectionPool = connectionPool;
+ }
+
+ /**
+ * Creates a new freeform query delegate to be used with the
+ * {@link SQLContainer}.
+ *
+ * @param queryString
+ * The actual query to perform.
+ * @param connectionPool
+ * the JDBCConnectionPool to use to open connections to the SQL
+ * database.
+ * @param primaryKeyColumns
+ * The primary key columns. Read-only mode is forced if none are
+ * provided. (optional)
+ */
+ public FreeformQuery(String queryString, JDBCConnectionPool connectionPool,
+ String... primaryKeyColumns) {
+ this(queryString, Arrays.asList(primaryKeyColumns), connectionPool);
+ }
+
+ /**
+ * This implementation of getCount() actually fetches all records from the
+ * database, which might be a performance issue. Override this method with a
+ * SELECT COUNT(*) ... query if this is too slow for your needs.
+ *
+ * {@inheritDoc}
+ */
+ @Override
+ public int getCount() throws SQLException {
+ // First try the delegate
+ int count = countByDelegate();
+ if (count < 0) {
+ // Couldn't use the delegate, use the bad way.
+ Connection conn = getConnection();
+ Statement statement = conn.createStatement(
+ ResultSet.TYPE_SCROLL_INSENSITIVE,
+ ResultSet.CONCUR_READ_ONLY);
+
+ ResultSet rs = statement.executeQuery(queryString);
+ if (rs.last()) {
+ count = rs.getRow();
+ } else {
+ count = 0;
+ }
+ rs.close();
+ statement.close();
+ releaseConnection(conn);
+ }
+ return count;
+ }
+
+ @SuppressWarnings("deprecation")
+ private int countByDelegate() throws SQLException {
+ int count = -1;
+ if (delegate == null) {
+ return count;
+ }
+ /* First try using prepared statement */
+ if (delegate instanceof FreeformStatementDelegate) {
+ try {
+ StatementHelper sh = ((FreeformStatementDelegate) delegate)
+ .getCountStatement();
+ Connection c = getConnection();
+ PreparedStatement pstmt = c.prepareStatement(sh
+ .getQueryString());
+ sh.setParameterValuesToStatement(pstmt);
+ ResultSet rs = pstmt.executeQuery();
+ rs.next();
+ count = rs.getInt(1);
+ rs.close();
+ pstmt.clearParameters();
+ pstmt.close();
+ releaseConnection(c);
+ return count;
+ } catch (UnsupportedOperationException e) {
+ // Count statement generation not supported
+ }
+ }
+ /* Try using regular statement */
+ try {
+ String countQuery = delegate.getCountQuery();
+ if (countQuery != null) {
+ Connection conn = getConnection();
+ Statement statement = conn.createStatement();
+ ResultSet rs = statement.executeQuery(countQuery);
+ rs.next();
+ count = rs.getInt(1);
+ rs.close();
+ statement.close();
+ releaseConnection(conn);
+ return count;
+ }
+ } catch (UnsupportedOperationException e) {
+ // Count query generation not supported
+ }
+ return count;
+ }
+
+ private Connection getConnection() throws SQLException {
+ if (activeConnection != null) {
+ return activeConnection;
+ }
+ return connectionPool.reserveConnection();
+ }
+
+ /**
+ * Fetches the results for the query. This implementation always fetches the
+ * entire record set, ignoring the offset and page length parameters. In
+ * order to support lazy loading of records, you must supply a
+ * FreeformQueryDelegate that implements the
+ * FreeformQueryDelegate.getQueryString(int,int) method.
+ *
+ * @throws SQLException
+ *
+ * @see FreeformQueryDelegate#getQueryString(int, int)
+ */
+ @Override
+ @SuppressWarnings("deprecation")
+ public ResultSet getResults(int offset, int pagelength) throws SQLException {
+ if (activeConnection == null) {
+ throw new SQLException("No active transaction!");
+ }
+ String query = queryString;
+ if (delegate != null) {
+ /* First try using prepared statement */
+ if (delegate instanceof FreeformStatementDelegate) {
+ try {
+ StatementHelper sh = ((FreeformStatementDelegate) delegate)
+ .getQueryStatement(offset, pagelength);
+ PreparedStatement pstmt = activeConnection
+ .prepareStatement(sh.getQueryString());
+ sh.setParameterValuesToStatement(pstmt);
+ return pstmt.executeQuery();
+ } catch (UnsupportedOperationException e) {
+ // Statement generation not supported, continue...
+ }
+ }
+ try {
+ query = delegate.getQueryString(offset, pagelength);
+ } catch (UnsupportedOperationException e) {
+ // This is fine, we'll just use the default queryString.
+ }
+ }
+ Statement statement = activeConnection.createStatement();
+ ResultSet rs = statement.executeQuery(query);
+ return rs;
+ }
+
+ @Override
+ @SuppressWarnings("deprecation")
+ public boolean implementationRespectsPagingLimits() {
+ if (delegate == null) {
+ return false;
+ }
+ /* First try using prepared statement */
+ if (delegate instanceof FreeformStatementDelegate) {
+ try {
+ StatementHelper sh = ((FreeformStatementDelegate) delegate)
+ .getCountStatement();
+ if (sh != null && sh.getQueryString() != null
+ && sh.getQueryString().length() > 0) {
+ return true;
+ }
+ } catch (UnsupportedOperationException e) {
+ // Statement generation not supported, continue...
+ }
+ }
+ try {
+ String queryString = delegate.getQueryString(0, 50);
+ return queryString != null && queryString.length() > 0;
+ } catch (UnsupportedOperationException e) {
+ return false;
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.data.util.sqlcontainer.query.QueryDelegate#setFilters(java
+ * .util.List)
+ */
+ @Override
+ public void setFilters(List<Filter> filters)
+ throws UnsupportedOperationException {
+ if (delegate != null) {
+ delegate.setFilters(filters);
+ } else if (filters != null) {
+ throw new UnsupportedOperationException(
+ "FreeFormQueryDelegate not set!");
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.data.util.sqlcontainer.query.QueryDelegate#setOrderBy(java
+ * .util.List)
+ */
+ @Override
+ public void setOrderBy(List<OrderBy> orderBys)
+ throws UnsupportedOperationException {
+ if (delegate != null) {
+ delegate.setOrderBy(orderBys);
+ } else if (orderBys != null) {
+ throw new UnsupportedOperationException(
+ "FreeFormQueryDelegate not set!");
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.data.util.sqlcontainer.query.QueryDelegate#storeRow(com.vaadin
+ * .data.util.sqlcontainer.RowItem)
+ */
+ @Override
+ public int storeRow(RowItem row) throws SQLException {
+ if (activeConnection == null) {
+ throw new IllegalStateException("No transaction is active!");
+ } else if (primaryKeyColumns.isEmpty()) {
+ throw new UnsupportedOperationException(
+ "Cannot store items fetched with a read-only freeform query!");
+ }
+ if (delegate != null) {
+ return delegate.storeRow(activeConnection, row);
+ } else {
+ throw new UnsupportedOperationException(
+ "FreeFormQueryDelegate not set!");
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.data.util.sqlcontainer.query.QueryDelegate#removeRow(com.vaadin
+ * .data.util.sqlcontainer.RowItem)
+ */
+ @Override
+ public boolean removeRow(RowItem row) throws SQLException {
+ if (activeConnection == null) {
+ throw new IllegalStateException("No transaction is active!");
+ } else if (primaryKeyColumns.isEmpty()) {
+ throw new UnsupportedOperationException(
+ "Cannot remove items fetched with a read-only freeform query!");
+ }
+ if (delegate != null) {
+ return delegate.removeRow(activeConnection, row);
+ } else {
+ throw new UnsupportedOperationException(
+ "FreeFormQueryDelegate not set!");
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.data.util.sqlcontainer.query.QueryDelegate#beginTransaction()
+ */
+ @Override
+ public synchronized void beginTransaction()
+ throws UnsupportedOperationException, SQLException {
+ if (activeConnection != null) {
+ throw new IllegalStateException("A transaction is already active!");
+ }
+ activeConnection = connectionPool.reserveConnection();
+ activeConnection.setAutoCommit(false);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.util.sqlcontainer.query.QueryDelegate#commit()
+ */
+ @Override
+ public synchronized void commit() throws UnsupportedOperationException,
+ SQLException {
+ if (activeConnection == null) {
+ throw new SQLException("No active transaction");
+ }
+ if (!activeConnection.getAutoCommit()) {
+ activeConnection.commit();
+ }
+ connectionPool.releaseConnection(activeConnection);
+ activeConnection = null;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.util.sqlcontainer.query.QueryDelegate#rollback()
+ */
+ @Override
+ public synchronized void rollback() throws UnsupportedOperationException,
+ SQLException {
+ if (activeConnection == null) {
+ throw new SQLException("No active transaction");
+ }
+ activeConnection.rollback();
+ connectionPool.releaseConnection(activeConnection);
+ activeConnection = null;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.data.util.sqlcontainer.query.QueryDelegate#getPrimaryKeyColumns
+ * ()
+ */
+ @Override
+ public List<String> getPrimaryKeyColumns() {
+ return primaryKeyColumns;
+ }
+
+ public String getQueryString() {
+ return queryString;
+ }
+
+ public FreeformQueryDelegate getDelegate() {
+ return delegate;
+ }
+
+ public void setDelegate(FreeformQueryDelegate delegate) {
+ this.delegate = delegate;
+ }
+
+ /**
+ * This implementation of the containsRowWithKey method rewrites existing
+ * WHERE clauses in the query string. The logic is, however, not very
+ * complex and some times can do the Wrong Thing<sup>TM</sup>. For the
+ * situations where this logic is not enough, you can implement the
+ * getContainsRowQueryString method in FreeformQueryDelegate and this will
+ * be used instead of the logic.
+ *
+ * @see FreeformQueryDelegate#getContainsRowQueryString(Object...)
+ *
+ */
+ @Override
+ @SuppressWarnings("deprecation")
+ public boolean containsRowWithKey(Object... keys) throws SQLException {
+ String query = null;
+ boolean contains = false;
+ if (delegate != null) {
+ if (delegate instanceof FreeformStatementDelegate) {
+ try {
+ StatementHelper sh = ((FreeformStatementDelegate) delegate)
+ .getContainsRowQueryStatement(keys);
+ Connection c = getConnection();
+ PreparedStatement pstmt = c.prepareStatement(sh
+ .getQueryString());
+ sh.setParameterValuesToStatement(pstmt);
+ ResultSet rs = pstmt.executeQuery();
+ contains = rs.next();
+ rs.close();
+ pstmt.clearParameters();
+ pstmt.close();
+ releaseConnection(c);
+ return contains;
+ } catch (UnsupportedOperationException e) {
+ // Statement generation not supported, continue...
+ }
+ }
+ try {
+ query = delegate.getContainsRowQueryString(keys);
+ } catch (UnsupportedOperationException e) {
+ query = modifyWhereClause(keys);
+ }
+ } else {
+ query = modifyWhereClause(keys);
+ }
+ Connection conn = getConnection();
+ try {
+ Statement statement = conn.createStatement();
+ ResultSet rs = statement.executeQuery(query);
+ contains = rs.next();
+ rs.close();
+ statement.close();
+ } finally {
+ releaseConnection(conn);
+ }
+ return contains;
+ }
+
+ /**
+ * Releases the connection if it is not part of an active transaction.
+ *
+ * @param conn
+ * the connection to release
+ */
+ private void releaseConnection(Connection conn) {
+ if (conn != activeConnection) {
+ connectionPool.releaseConnection(conn);
+ }
+ }
+
+ private String modifyWhereClause(Object... keys) {
+ // Build the where rules for the provided keys
+ StringBuffer where = new StringBuffer();
+ for (int ix = 0; ix < primaryKeyColumns.size(); ix++) {
+ where.append(QueryBuilder.quote(primaryKeyColumns.get(ix)));
+ if (keys[ix] == null) {
+ where.append(" IS NULL");
+ } else {
+ where.append(" = '").append(keys[ix]).append("'");
+ }
+ if (ix < primaryKeyColumns.size() - 1) {
+ where.append(" AND ");
+ }
+ }
+ // Is there already a WHERE clause in the query string?
+ int index = queryString.toLowerCase().indexOf("where ");
+ if (index > -1) {
+ // Rewrite the where clause
+ return queryString.substring(0, index) + "WHERE " + where + " AND "
+ + queryString.substring(index + 6);
+ }
+ // Append a where clause
+ return queryString + " WHERE " + where;
+ }
+
+ private void writeObject(java.io.ObjectOutputStream out) throws IOException {
+ try {
+ rollback();
+ } catch (SQLException ignored) {
+ }
+ out.defaultWriteObject();
+ }
+}
diff --git a/server/src/com/vaadin/data/util/sqlcontainer/query/FreeformQueryDelegate.java b/server/src/com/vaadin/data/util/sqlcontainer/query/FreeformQueryDelegate.java
new file mode 100644
index 0000000000..433d742be8
--- /dev/null
+++ b/server/src/com/vaadin/data/util/sqlcontainer/query/FreeformQueryDelegate.java
@@ -0,0 +1,118 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util.sqlcontainer.query;
+
+import java.io.Serializable;
+import java.sql.Connection;
+import java.sql.SQLException;
+import java.util.List;
+
+import com.vaadin.data.Container.Filter;
+import com.vaadin.data.util.sqlcontainer.RowItem;
+
+public interface FreeformQueryDelegate extends Serializable {
+ /**
+ * Should return the SQL query string to be performed. This method is
+ * responsible for gluing together the select query from the filters and the
+ * order by conditions if these are supported.
+ *
+ * @param offset
+ * the first record (row) to fetch.
+ * @param pagelength
+ * the number of records (rows) to fetch. 0 means all records
+ * starting from offset.
+ * @deprecated Implement {@link FreeformStatementDelegate} instead of
+ * {@link FreeformQueryDelegate}
+ */
+ @Deprecated
+ public String getQueryString(int offset, int limit)
+ throws UnsupportedOperationException;
+
+ /**
+ * Generates and executes a query to determine the current row count from
+ * the DB. Row count will be fetched using filters that are currently set to
+ * the QueryDelegate.
+ *
+ * @return row count
+ * @throws SQLException
+ * @deprecated Implement {@link FreeformStatementDelegate} instead of
+ * {@link FreeformQueryDelegate}
+ */
+ @Deprecated
+ public String getCountQuery() throws UnsupportedOperationException;
+
+ /**
+ * Sets the filters to apply when performing the SQL query. These are
+ * translated into a WHERE clause. Default filtering mode will be used.
+ *
+ * @param filters
+ * The filters to apply.
+ * @throws UnsupportedOperationException
+ * if the implementation doesn't support filtering.
+ */
+ public void setFilters(List<Filter> filters)
+ throws UnsupportedOperationException;
+
+ /**
+ * Sets the order in which to retrieve rows from the database. The result
+ * can be ordered by zero or more columns and each column can be in
+ * ascending or descending order. These are translated into an ORDER BY
+ * clause in the SQL query.
+ *
+ * @param orderBys
+ * A list of the OrderBy conditions.
+ * @throws UnsupportedOperationException
+ * if the implementation doesn't support ordering.
+ */
+ public void setOrderBy(List<OrderBy> orderBys)
+ throws UnsupportedOperationException;
+
+ /**
+ * Stores a row in the database. The implementation of this interface
+ * decides how to identify whether to store a new row or update an existing
+ * one.
+ *
+ * @param conn
+ * the JDBC connection to use
+ * @param row
+ * RowItem to be stored or updated.
+ * @throws UnsupportedOperationException
+ * if the implementation is read only.
+ * @throws SQLException
+ */
+ public int storeRow(Connection conn, RowItem row)
+ throws UnsupportedOperationException, SQLException;
+
+ /**
+ * Removes the given RowItem from the database.
+ *
+ * @param conn
+ * the JDBC connection to use
+ * @param row
+ * RowItem to be removed
+ * @return true on success
+ * @throws UnsupportedOperationException
+ * @throws SQLException
+ */
+ public boolean removeRow(Connection conn, RowItem row)
+ throws UnsupportedOperationException, SQLException;
+
+ /**
+ * Generates an SQL Query string that allows the user of the FreeformQuery
+ * class to customize the query string used by the
+ * FreeformQuery.containsRowWithKeys() method. This is useful for cases when
+ * the logic in the containsRowWithKeys method is not enough to support more
+ * complex free form queries.
+ *
+ * @param keys
+ * the values of the primary keys
+ * @throws UnsupportedOperationException
+ * to use the default logic in FreeformQuery
+ * @deprecated Implement {@link FreeformStatementDelegate} instead of
+ * {@link FreeformQueryDelegate}
+ */
+ @Deprecated
+ public String getContainsRowQueryString(Object... keys)
+ throws UnsupportedOperationException;
+}
diff --git a/server/src/com/vaadin/data/util/sqlcontainer/query/FreeformStatementDelegate.java b/server/src/com/vaadin/data/util/sqlcontainer/query/FreeformStatementDelegate.java
new file mode 100644
index 0000000000..95521c5019
--- /dev/null
+++ b/server/src/com/vaadin/data/util/sqlcontainer/query/FreeformStatementDelegate.java
@@ -0,0 +1,57 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util.sqlcontainer.query;
+
+import com.vaadin.data.util.sqlcontainer.query.generator.StatementHelper;
+
+/**
+ * FreeformStatementDelegate is an extension to FreeformQueryDelegate that
+ * provides definitions for methods that produce StatementHelper objects instead
+ * of basic query strings. This allows the FreeformQuery query delegate to use
+ * PreparedStatements instead of regular Statement when accessing the database.
+ *
+ * Due to the injection protection and other benefits of prepared statements, it
+ * is advisable to implement this interface instead of the FreeformQueryDelegate
+ * whenever possible.
+ */
+public interface FreeformStatementDelegate extends FreeformQueryDelegate {
+ /**
+ * Should return a new instance of StatementHelper that contains the query
+ * string and parameter values required to create a PreparedStatement. This
+ * method is responsible for gluing together the select query from the
+ * filters and the order by conditions if these are supported.
+ *
+ * @param offset
+ * the first record (row) to fetch.
+ * @param pagelength
+ * the number of records (rows) to fetch. 0 means all records
+ * starting from offset.
+ */
+ public StatementHelper getQueryStatement(int offset, int limit)
+ throws UnsupportedOperationException;
+
+ /**
+ * Should return a new instance of StatementHelper that contains the query
+ * string and parameter values required to create a PreparedStatement that
+ * will fetch the row count from the DB. Row count should be fetched using
+ * filters that are currently set to the QueryDelegate.
+ */
+ public StatementHelper getCountStatement()
+ throws UnsupportedOperationException;
+
+ /**
+ * Should return a new instance of StatementHelper that contains the query
+ * string and parameter values required to create a PreparedStatement used
+ * by the FreeformQuery.containsRowWithKeys() method. This is useful for
+ * cases when the default logic in said method is not enough to support more
+ * complex free form queries.
+ *
+ * @param keys
+ * the values of the primary keys
+ * @throws UnsupportedOperationException
+ * to use the default logic in FreeformQuery
+ */
+ public StatementHelper getContainsRowQueryStatement(Object... keys)
+ throws UnsupportedOperationException;
+}
diff --git a/server/src/com/vaadin/data/util/sqlcontainer/query/OrderBy.java b/server/src/com/vaadin/data/util/sqlcontainer/query/OrderBy.java
new file mode 100644
index 0000000000..8ebe10067e
--- /dev/null
+++ b/server/src/com/vaadin/data/util/sqlcontainer/query/OrderBy.java
@@ -0,0 +1,46 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util.sqlcontainer.query;
+
+import java.io.Serializable;
+
+/**
+ * OrderBy represents a sorting rule to be applied to a query made by the
+ * SQLContainer's QueryDelegate.
+ *
+ * The sorting rule is simple and contains only the affected column's name and
+ * the direction of the sort.
+ */
+public class OrderBy implements Serializable {
+ private String column;
+ private boolean isAscending;
+
+ /**
+ * Prevent instantiation without required parameters.
+ */
+ @SuppressWarnings("unused")
+ private OrderBy() {
+ }
+
+ public OrderBy(String column, boolean isAscending) {
+ setColumn(column);
+ setAscending(isAscending);
+ }
+
+ public void setColumn(String column) {
+ this.column = column;
+ }
+
+ public String getColumn() {
+ return column;
+ }
+
+ public void setAscending(boolean isAscending) {
+ this.isAscending = isAscending;
+ }
+
+ public boolean isAscending() {
+ return isAscending;
+ }
+}
diff --git a/server/src/com/vaadin/data/util/sqlcontainer/query/QueryDelegate.java b/server/src/com/vaadin/data/util/sqlcontainer/query/QueryDelegate.java
new file mode 100644
index 0000000000..6e4396fad1
--- /dev/null
+++ b/server/src/com/vaadin/data/util/sqlcontainer/query/QueryDelegate.java
@@ -0,0 +1,211 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util.sqlcontainer.query;
+
+import java.io.Serializable;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.List;
+
+import com.vaadin.data.Container.Filter;
+import com.vaadin.data.util.sqlcontainer.RowId;
+import com.vaadin.data.util.sqlcontainer.RowItem;
+
+public interface QueryDelegate extends Serializable {
+ /**
+ * Generates and executes a query to determine the current row count from
+ * the DB. Row count will be fetched using filters that are currently set to
+ * the QueryDelegate.
+ *
+ * @return row count
+ * @throws SQLException
+ */
+ public int getCount() throws SQLException;
+
+ /**
+ * Executes a paged SQL query and returns the ResultSet. The query is
+ * defined through implementations of this QueryDelegate interface.
+ *
+ * @param offset
+ * the first item of the page to load
+ * @param pagelength
+ * the length of the page to load
+ * @return a ResultSet containing the rows of the page
+ * @throws SQLException
+ * if the database access fails.
+ */
+ public ResultSet getResults(int offset, int pagelength) throws SQLException;
+
+ /**
+ * Allows the SQLContainer implementation to check whether the QueryDelegate
+ * implementation implements paging in the getResults method.
+ *
+ * @see QueryDelegate#getResults(int, int)
+ *
+ * @return true if the delegate implements paging
+ */
+ public boolean implementationRespectsPagingLimits();
+
+ /**
+ * Sets the filters to apply when performing the SQL query. These are
+ * translated into a WHERE clause. Default filtering mode will be used.
+ *
+ * @param filters
+ * The filters to apply.
+ * @throws UnsupportedOperationException
+ * if the implementation doesn't support filtering.
+ */
+ public void setFilters(List<Filter> filters)
+ throws UnsupportedOperationException;
+
+ /**
+ * Sets the order in which to retrieve rows from the database. The result
+ * can be ordered by zero or more columns and each column can be in
+ * ascending or descending order. These are translated into an ORDER BY
+ * clause in the SQL query.
+ *
+ * @param orderBys
+ * A list of the OrderBy conditions.
+ * @throws UnsupportedOperationException
+ * if the implementation doesn't support ordering.
+ */
+ public void setOrderBy(List<OrderBy> orderBys)
+ throws UnsupportedOperationException;
+
+ /**
+ * Stores a row in the database. The implementation of this interface
+ * decides how to identify whether to store a new row or update an existing
+ * one.
+ *
+ * @param columnToValueMap
+ * A map containing the values for all columns to be stored or
+ * updated.
+ * @return the number of affected rows in the database table
+ * @throws UnsupportedOperationException
+ * if the implementation is read only.
+ */
+ public int storeRow(RowItem row) throws UnsupportedOperationException,
+ SQLException;
+
+ /**
+ * Removes the given RowItem from the database.
+ *
+ * @param row
+ * RowItem to be removed
+ * @return true on success
+ * @throws UnsupportedOperationException
+ * @throws SQLException
+ */
+ public boolean removeRow(RowItem row) throws UnsupportedOperationException,
+ SQLException;
+
+ /**
+ * Starts a new database transaction. Used when storing multiple changes.
+ *
+ * Note that if a transaction is already open, it will be rolled back when a
+ * new transaction is started.
+ *
+ * @throws SQLException
+ * if the database access fails.
+ */
+ public void beginTransaction() throws SQLException;
+
+ /**
+ * Commits a transaction. If a transaction is not open nothing should
+ * happen.
+ *
+ * @throws SQLException
+ * if the database access fails.
+ */
+ public void commit() throws SQLException;
+
+ /**
+ * Rolls a transaction back. If a transaction is not open nothing should
+ * happen.
+ *
+ * @throws SQLException
+ * if the database access fails.
+ */
+ public void rollback() throws SQLException;
+
+ /**
+ * Returns a list of primary key column names. The list is either fetched
+ * from the database (TableQuery) or given as an argument depending on
+ * implementation.
+ *
+ * @return
+ */
+ public List<String> getPrimaryKeyColumns();
+
+ /**
+ * Performs a query to find out whether the SQL table contains a row with
+ * the given set of primary keys.
+ *
+ * @param keys
+ * the primary keys
+ * @return true if the SQL table contains a row with the provided keys
+ * @throws SQLException
+ */
+ public boolean containsRowWithKey(Object... keys) throws SQLException;
+
+ /************************/
+ /** ROWID CHANGE EVENT **/
+ /************************/
+
+ /**
+ * An <code>Event</code> object specifying the old and new RowId of an added
+ * item after the addition has been successfully committed.
+ */
+ public interface RowIdChangeEvent extends Serializable {
+ /**
+ * Gets the old (temporary) RowId of the added row that raised this
+ * event.
+ *
+ * @return old RowId
+ */
+ public RowId getOldRowId();
+
+ /**
+ * Gets the new, possibly database assigned RowId of the added row that
+ * raised this event.
+ *
+ * @return new RowId
+ */
+ public RowId getNewRowId();
+ }
+
+ /** RowId change listener interface. */
+ public interface RowIdChangeListener extends Serializable {
+ /**
+ * Lets the listener know that a RowId has been changed.
+ *
+ * @param event
+ */
+ public void rowIdChange(QueryDelegate.RowIdChangeEvent event);
+ }
+
+ /**
+ * The interface for adding and removing <code>RowIdChangeEvent</code>
+ * listeners. By implementing this interface a class explicitly announces
+ * that it will generate a <code>RowIdChangeEvent</code> when it performs a
+ * database commit that may change the RowId.
+ */
+ public interface RowIdChangeNotifier extends Serializable {
+ /**
+ * Adds a RowIdChangeListener for the object.
+ *
+ * @param listener
+ * listener to be added
+ */
+ public void addListener(QueryDelegate.RowIdChangeListener listener);
+
+ /**
+ * Removes the specified RowIdChangeListener from the object.
+ *
+ * @param listener
+ * listener to be removed
+ */
+ public void removeListener(QueryDelegate.RowIdChangeListener listener);
+ }
+}
diff --git a/server/src/com/vaadin/data/util/sqlcontainer/query/TableQuery.java b/server/src/com/vaadin/data/util/sqlcontainer/query/TableQuery.java
new file mode 100644
index 0000000000..d0606704f7
--- /dev/null
+++ b/server/src/com/vaadin/data/util/sqlcontainer/query/TableQuery.java
@@ -0,0 +1,715 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util.sqlcontainer.query;
+
+import java.io.IOException;
+import java.sql.Connection;
+import java.sql.DatabaseMetaData;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.ResultSetMetaData;
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.EventObject;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import com.vaadin.data.Container.Filter;
+import com.vaadin.data.util.filter.Compare.Equal;
+import com.vaadin.data.util.sqlcontainer.ColumnProperty;
+import com.vaadin.data.util.sqlcontainer.OptimisticLockException;
+import com.vaadin.data.util.sqlcontainer.RowId;
+import com.vaadin.data.util.sqlcontainer.RowItem;
+import com.vaadin.data.util.sqlcontainer.SQLUtil;
+import com.vaadin.data.util.sqlcontainer.TemporaryRowId;
+import com.vaadin.data.util.sqlcontainer.connection.JDBCConnectionPool;
+import com.vaadin.data.util.sqlcontainer.query.generator.DefaultSQLGenerator;
+import com.vaadin.data.util.sqlcontainer.query.generator.MSSQLGenerator;
+import com.vaadin.data.util.sqlcontainer.query.generator.SQLGenerator;
+import com.vaadin.data.util.sqlcontainer.query.generator.StatementHelper;
+
+@SuppressWarnings("serial")
+public class TableQuery implements QueryDelegate,
+ QueryDelegate.RowIdChangeNotifier {
+
+ /** Table name, primary key column name(s) and version column name */
+ private String tableName;
+ private List<String> primaryKeyColumns;
+ private String versionColumn;
+
+ /** Currently set Filters and OrderBys */
+ private List<Filter> filters;
+ private List<OrderBy> orderBys;
+
+ /** SQLGenerator instance to use for generating queries */
+ private SQLGenerator sqlGenerator;
+
+ /** Fields related to Connection and Transaction handling */
+ private JDBCConnectionPool connectionPool;
+ private transient Connection activeConnection;
+ private boolean transactionOpen;
+
+ /** Row ID change listeners */
+ private LinkedList<RowIdChangeListener> rowIdChangeListeners;
+ /** Row ID change events, stored until commit() is called */
+ private final List<RowIdChangeEvent> bufferedEvents = new ArrayList<RowIdChangeEvent>();
+
+ /** Set to true to output generated SQL Queries to System.out */
+ private boolean debug = false;
+
+ /** Prevent no-parameters instantiation of TableQuery */
+ @SuppressWarnings("unused")
+ private TableQuery() {
+ }
+
+ /**
+ * Creates a new TableQuery using the given connection pool, SQL generator
+ * and table name to fetch the data from. All parameters must be non-null.
+ *
+ * @param tableName
+ * Name of the database table to connect to
+ * @param connectionPool
+ * Connection pool for accessing the database
+ * @param sqlGenerator
+ * SQL query generator implementation
+ */
+ public TableQuery(String tableName, JDBCConnectionPool connectionPool,
+ SQLGenerator sqlGenerator) {
+ if (tableName == null || tableName.trim().length() < 1
+ || connectionPool == null || sqlGenerator == null) {
+ throw new IllegalArgumentException(
+ "All parameters must be non-null and a table name must be given.");
+ }
+ this.tableName = tableName;
+ this.sqlGenerator = sqlGenerator;
+ this.connectionPool = connectionPool;
+ fetchMetaData();
+ }
+
+ /**
+ * Creates a new TableQuery using the given connection pool and table name
+ * to fetch the data from. All parameters must be non-null. The default SQL
+ * generator will be used for queries.
+ *
+ * @param tableName
+ * Name of the database table to connect to
+ * @param connectionPool
+ * Connection pool for accessing the database
+ */
+ public TableQuery(String tableName, JDBCConnectionPool connectionPool) {
+ this(tableName, connectionPool, new DefaultSQLGenerator());
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.addon.sqlcontainer.query.QueryDelegate#getCount()
+ */
+ @Override
+ public int getCount() throws SQLException {
+ getLogger().log(Level.FINE, "Fetching count...");
+ StatementHelper sh = sqlGenerator.generateSelectQuery(tableName,
+ filters, null, 0, 0, "COUNT(*)");
+ boolean shouldCloseTransaction = false;
+ if (!transactionOpen) {
+ shouldCloseTransaction = true;
+ beginTransaction();
+ }
+ ResultSet r = executeQuery(sh);
+ r.next();
+ int count = r.getInt(1);
+ r.getStatement().close();
+ r.close();
+ if (shouldCloseTransaction) {
+ commit();
+ }
+ return count;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.addon.sqlcontainer.query.QueryDelegate#getResults(int,
+ * int)
+ */
+ @Override
+ public ResultSet getResults(int offset, int pagelength) throws SQLException {
+ StatementHelper sh;
+ /*
+ * If no ordering is explicitly set, results will be ordered by the
+ * first primary key column.
+ */
+ if (orderBys == null || orderBys.isEmpty()) {
+ List<OrderBy> ob = new ArrayList<OrderBy>();
+ ob.add(new OrderBy(primaryKeyColumns.get(0), true));
+ sh = sqlGenerator.generateSelectQuery(tableName, filters, ob,
+ offset, pagelength, null);
+ } else {
+ sh = sqlGenerator.generateSelectQuery(tableName, filters, orderBys,
+ offset, pagelength, null);
+ }
+ return executeQuery(sh);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.addon.sqlcontainer.query.QueryDelegate#
+ * implementationRespectsPagingLimits()
+ */
+ @Override
+ public boolean implementationRespectsPagingLimits() {
+ return true;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.addon.sqlcontainer.query.QueryDelegate#storeRow(com.vaadin
+ * .addon.sqlcontainer.RowItem)
+ */
+ @Override
+ public int storeRow(RowItem row) throws UnsupportedOperationException,
+ SQLException {
+ if (row == null) {
+ throw new IllegalArgumentException("Row argument must be non-null.");
+ }
+ StatementHelper sh;
+ int result = 0;
+ if (row.getId() instanceof TemporaryRowId) {
+ setVersionColumnFlagInProperty(row);
+ sh = sqlGenerator.generateInsertQuery(tableName, row);
+ result = executeUpdateReturnKeys(sh, row);
+ } else {
+ setVersionColumnFlagInProperty(row);
+ sh = sqlGenerator.generateUpdateQuery(tableName, row);
+ result = executeUpdate(sh);
+ }
+ if (versionColumn != null && result == 0) {
+ throw new OptimisticLockException(
+ "Someone else changed the row that was being updated.",
+ row.getId());
+ }
+ return result;
+ }
+
+ private void setVersionColumnFlagInProperty(RowItem row) {
+ ColumnProperty versionProperty = (ColumnProperty) row
+ .getItemProperty(versionColumn);
+ if (versionProperty != null) {
+ versionProperty.setVersionColumn(true);
+ }
+ }
+
+ /**
+ * Inserts the given row in the database table immediately. Begins and
+ * commits the transaction needed. This method was added specifically to
+ * solve the problem of returning the final RowId immediately on the
+ * SQLContainer.addItem() call when auto commit mode is enabled in the
+ * SQLContainer.
+ *
+ * @param row
+ * RowItem to add to the database
+ * @return Final RowId of the added row
+ * @throws SQLException
+ */
+ public RowId storeRowImmediately(RowItem row) throws SQLException {
+ beginTransaction();
+ /* Set version column, if one is provided */
+ setVersionColumnFlagInProperty(row);
+ /* Generate query */
+ StatementHelper sh = sqlGenerator.generateInsertQuery(tableName, row);
+ PreparedStatement pstmt = activeConnection.prepareStatement(
+ sh.getQueryString(), primaryKeyColumns.toArray(new String[0]));
+ sh.setParameterValuesToStatement(pstmt);
+ getLogger().log(Level.FINE, "DB -> " + sh.getQueryString());
+ int result = pstmt.executeUpdate();
+ if (result > 0) {
+ /*
+ * If affected rows exist, we'll get the new RowId, commit the
+ * transaction and return the new RowId.
+ */
+ ResultSet generatedKeys = pstmt.getGeneratedKeys();
+ RowId newId = getNewRowId(row, generatedKeys);
+ generatedKeys.close();
+ pstmt.clearParameters();
+ pstmt.close();
+ commit();
+ return newId;
+ } else {
+ pstmt.clearParameters();
+ pstmt.close();
+ /* On failure return null */
+ return null;
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.addon.sqlcontainer.query.QueryDelegate#setFilters(java.util
+ * .List)
+ */
+ @Override
+ public void setFilters(List<Filter> filters)
+ throws UnsupportedOperationException {
+ if (filters == null) {
+ this.filters = null;
+ return;
+ }
+ this.filters = Collections.unmodifiableList(filters);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.addon.sqlcontainer.query.QueryDelegate#setOrderBy(java.util
+ * .List)
+ */
+ @Override
+ public void setOrderBy(List<OrderBy> orderBys)
+ throws UnsupportedOperationException {
+ if (orderBys == null) {
+ this.orderBys = null;
+ return;
+ }
+ this.orderBys = Collections.unmodifiableList(orderBys);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.addon.sqlcontainer.query.QueryDelegate#beginTransaction()
+ */
+ @Override
+ public void beginTransaction() throws UnsupportedOperationException,
+ SQLException {
+ if (transactionOpen && activeConnection != null) {
+ throw new IllegalStateException();
+ }
+
+ getLogger().log(Level.FINE, "DB -> begin transaction");
+ activeConnection = connectionPool.reserveConnection();
+ activeConnection.setAutoCommit(false);
+ transactionOpen = true;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.addon.sqlcontainer.query.QueryDelegate#commit()
+ */
+ @Override
+ public void commit() throws UnsupportedOperationException, SQLException {
+ if (transactionOpen && activeConnection != null) {
+ getLogger().log(Level.FINE, "DB -> commit");
+ activeConnection.commit();
+ connectionPool.releaseConnection(activeConnection);
+ } else {
+ throw new SQLException("No active transaction");
+ }
+ transactionOpen = false;
+
+ /* Handle firing row ID change events */
+ RowIdChangeEvent[] unFiredEvents = bufferedEvents
+ .toArray(new RowIdChangeEvent[] {});
+ bufferedEvents.clear();
+ if (rowIdChangeListeners != null && !rowIdChangeListeners.isEmpty()) {
+ for (RowIdChangeListener r : rowIdChangeListeners) {
+ for (RowIdChangeEvent e : unFiredEvents) {
+ r.rowIdChange(e);
+ }
+ }
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.addon.sqlcontainer.query.QueryDelegate#rollback()
+ */
+ @Override
+ public void rollback() throws UnsupportedOperationException, SQLException {
+ if (transactionOpen && activeConnection != null) {
+ getLogger().log(Level.FINE, "DB -> rollback");
+ activeConnection.rollback();
+ connectionPool.releaseConnection(activeConnection);
+ } else {
+ throw new SQLException("No active transaction");
+ }
+ transactionOpen = false;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.addon.sqlcontainer.query.QueryDelegate#getPrimaryKeyColumns()
+ */
+ @Override
+ public List<String> getPrimaryKeyColumns() {
+ return Collections.unmodifiableList(primaryKeyColumns);
+ }
+
+ public String getVersionColumn() {
+ return versionColumn;
+ }
+
+ public void setVersionColumn(String column) {
+ versionColumn = column;
+ }
+
+ public String getTableName() {
+ return tableName;
+ }
+
+ public SQLGenerator getSqlGenerator() {
+ return sqlGenerator;
+ }
+
+ /**
+ * Executes the given query string using either the active connection if a
+ * transaction is already open, or a new connection from this query's
+ * connection pool.
+ *
+ * @param sh
+ * an instance of StatementHelper, containing the query string
+ * and parameter values.
+ * @return ResultSet of the query
+ * @throws SQLException
+ */
+ private ResultSet executeQuery(StatementHelper sh) throws SQLException {
+ Connection c = null;
+ if (transactionOpen && activeConnection != null) {
+ c = activeConnection;
+ } else {
+ throw new SQLException("No active transaction!");
+ }
+ PreparedStatement pstmt = c.prepareStatement(sh.getQueryString());
+ sh.setParameterValuesToStatement(pstmt);
+ getLogger().log(Level.FINE, "DB -> " + sh.getQueryString());
+ return pstmt.executeQuery();
+ }
+
+ /**
+ * Executes the given update query string using either the active connection
+ * if a transaction is already open, or a new connection from this query's
+ * connection pool.
+ *
+ * @param sh
+ * an instance of StatementHelper, containing the query string
+ * and parameter values.
+ * @return Number of affected rows
+ * @throws SQLException
+ */
+ private int executeUpdate(StatementHelper sh) throws SQLException {
+ Connection c = null;
+ PreparedStatement pstmt = null;
+ try {
+ if (transactionOpen && activeConnection != null) {
+ c = activeConnection;
+ } else {
+ c = connectionPool.reserveConnection();
+ }
+ pstmt = c.prepareStatement(sh.getQueryString());
+ sh.setParameterValuesToStatement(pstmt);
+ getLogger().log(Level.FINE, "DB -> " + sh.getQueryString());
+ int retval = pstmt.executeUpdate();
+ return retval;
+ } finally {
+ if (pstmt != null) {
+ pstmt.clearParameters();
+ pstmt.close();
+ }
+ if (!transactionOpen) {
+ connectionPool.releaseConnection(c);
+ }
+ }
+ }
+
+ /**
+ * Executes the given update query string using either the active connection
+ * if a transaction is already open, or a new connection from this query's
+ * connection pool.
+ *
+ * Additionally adds a new RowIdChangeEvent to the event buffer.
+ *
+ * @param sh
+ * an instance of StatementHelper, containing the query string
+ * and parameter values.
+ * @param row
+ * the row item to update
+ * @return Number of affected rows
+ * @throws SQLException
+ */
+ private int executeUpdateReturnKeys(StatementHelper sh, RowItem row)
+ throws SQLException {
+ Connection c = null;
+ PreparedStatement pstmt = null;
+ ResultSet genKeys = null;
+ try {
+ if (transactionOpen && activeConnection != null) {
+ c = activeConnection;
+ } else {
+ c = connectionPool.reserveConnection();
+ }
+ pstmt = c.prepareStatement(sh.getQueryString(),
+ primaryKeyColumns.toArray(new String[0]));
+ sh.setParameterValuesToStatement(pstmt);
+ getLogger().log(Level.FINE, "DB -> " + sh.getQueryString());
+ int result = pstmt.executeUpdate();
+ genKeys = pstmt.getGeneratedKeys();
+ RowId newId = getNewRowId(row, genKeys);
+ bufferedEvents.add(new RowIdChangeEvent(row.getId(), newId));
+ return result;
+ } finally {
+ if (genKeys != null) {
+ genKeys.close();
+ }
+ if (pstmt != null) {
+ pstmt.clearParameters();
+ pstmt.close();
+ }
+ if (!transactionOpen) {
+ connectionPool.releaseConnection(c);
+ }
+ }
+ }
+
+ /**
+ * Fetches name(s) of primary key column(s) from DB metadata.
+ *
+ * Also tries to get the escape string to be used in search strings.
+ */
+ private void fetchMetaData() {
+ Connection c = null;
+ try {
+ c = connectionPool.reserveConnection();
+ DatabaseMetaData dbmd = c.getMetaData();
+ if (dbmd != null) {
+ tableName = SQLUtil.escapeSQL(tableName);
+ ResultSet tables = dbmd.getTables(null, null, tableName, null);
+ if (!tables.next()) {
+ tables = dbmd.getTables(null, null,
+ tableName.toUpperCase(), null);
+ if (!tables.next()) {
+ throw new IllegalArgumentException(
+ "Table with the name \""
+ + tableName
+ + "\" was not found. Check your database contents.");
+ } else {
+ tableName = tableName.toUpperCase();
+ }
+ }
+ tables.close();
+ ResultSet rs = dbmd.getPrimaryKeys(null, null, tableName);
+ List<String> names = new ArrayList<String>();
+ while (rs.next()) {
+ names.add(rs.getString("COLUMN_NAME"));
+ }
+ rs.close();
+ if (!names.isEmpty()) {
+ primaryKeyColumns = names;
+ }
+ if (primaryKeyColumns == null || primaryKeyColumns.isEmpty()) {
+ throw new IllegalArgumentException(
+ "Primary key constraints have not been defined for the table \""
+ + tableName
+ + "\". Use FreeFormQuery to access this table.");
+ }
+ for (String colName : primaryKeyColumns) {
+ if (colName.equalsIgnoreCase("rownum")) {
+ if (getSqlGenerator() instanceof MSSQLGenerator
+ || getSqlGenerator() instanceof MSSQLGenerator) {
+ throw new IllegalArgumentException(
+ "When using Oracle or MSSQL, a primary key column"
+ + " named \'rownum\' is not allowed!");
+ }
+ }
+ }
+ }
+ } catch (SQLException e) {
+ throw new RuntimeException(e);
+ } finally {
+ connectionPool.releaseConnection(c);
+ }
+ }
+
+ private RowId getNewRowId(RowItem row, ResultSet genKeys) {
+ try {
+ /* Fetch primary key values and generate a map out of them. */
+ Map<String, Object> values = new HashMap<String, Object>();
+ ResultSetMetaData rsmd = genKeys.getMetaData();
+ int colCount = rsmd.getColumnCount();
+ if (genKeys.next()) {
+ for (int i = 1; i <= colCount; i++) {
+ values.put(rsmd.getColumnName(i), genKeys.getObject(i));
+ }
+ }
+ /* Generate new RowId */
+ List<Object> newRowId = new ArrayList<Object>();
+ if (values.size() == 1) {
+ if (primaryKeyColumns.size() == 1) {
+ newRowId.add(values.get(values.keySet().iterator().next()));
+ } else {
+ for (String s : primaryKeyColumns) {
+ if (!((ColumnProperty) row.getItemProperty(s))
+ .isReadOnlyChangeAllowed()) {
+ newRowId.add(values.get(values.keySet().iterator()
+ .next()));
+ } else {
+ newRowId.add(values.get(s));
+ }
+ }
+ }
+ } else {
+ for (String s : primaryKeyColumns) {
+ newRowId.add(values.get(s));
+ }
+ }
+ return new RowId(newRowId.toArray());
+ } catch (Exception e) {
+ getLogger().log(Level.FINE,
+ "Failed to fetch key values on insert: " + e.getMessage());
+ return null;
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.addon.sqlcontainer.query.QueryDelegate#removeRow(com.vaadin
+ * .addon.sqlcontainer.RowItem)
+ */
+ @Override
+ public boolean removeRow(RowItem row) throws UnsupportedOperationException,
+ SQLException {
+ getLogger().log(Level.FINE,
+ "Removing row with id: " + row.getId().getId()[0].toString());
+ if (executeUpdate(sqlGenerator.generateDeleteQuery(getTableName(),
+ primaryKeyColumns, versionColumn, row)) == 1) {
+ return true;
+ }
+ if (versionColumn != null) {
+ throw new OptimisticLockException(
+ "Someone else changed the row that was being deleted.",
+ row.getId());
+ }
+ return false;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.addon.sqlcontainer.query.QueryDelegate#containsRowWithKey(
+ * java.lang.Object[])
+ */
+ @Override
+ public boolean containsRowWithKey(Object... keys) throws SQLException {
+ ArrayList<Filter> filtersAndKeys = new ArrayList<Filter>();
+ if (filters != null) {
+ filtersAndKeys.addAll(filters);
+ }
+ int ix = 0;
+ for (String colName : primaryKeyColumns) {
+ filtersAndKeys.add(new Equal(colName, keys[ix]));
+ ix++;
+ }
+ StatementHelper sh = sqlGenerator.generateSelectQuery(tableName,
+ filtersAndKeys, orderBys, 0, 0, "*");
+
+ boolean shouldCloseTransaction = false;
+ if (!transactionOpen) {
+ shouldCloseTransaction = true;
+ beginTransaction();
+ }
+ ResultSet rs = null;
+ try {
+ rs = executeQuery(sh);
+ boolean contains = rs.next();
+ return contains;
+ } finally {
+ if (rs != null) {
+ if (rs.getStatement() != null) {
+ rs.getStatement().close();
+ }
+ rs.close();
+ }
+ if (shouldCloseTransaction) {
+ commit();
+ }
+ }
+ }
+
+ /**
+ * Custom writeObject to call rollback() if object is serialized.
+ */
+ private void writeObject(java.io.ObjectOutputStream out) throws IOException {
+ try {
+ rollback();
+ } catch (SQLException ignored) {
+ }
+ out.defaultWriteObject();
+ }
+
+ /**
+ * Simple RowIdChangeEvent implementation.
+ */
+ public class RowIdChangeEvent extends EventObject implements
+ QueryDelegate.RowIdChangeEvent {
+ private final RowId oldId;
+ private final RowId newId;
+
+ private RowIdChangeEvent(RowId oldId, RowId newId) {
+ super(oldId);
+ this.oldId = oldId;
+ this.newId = newId;
+ }
+
+ @Override
+ public RowId getNewRowId() {
+ return newId;
+ }
+
+ @Override
+ public RowId getOldRowId() {
+ return oldId;
+ }
+ }
+
+ /**
+ * Adds RowIdChangeListener to this query
+ */
+ @Override
+ public void addListener(RowIdChangeListener listener) {
+ if (rowIdChangeListeners == null) {
+ rowIdChangeListeners = new LinkedList<QueryDelegate.RowIdChangeListener>();
+ }
+ rowIdChangeListeners.add(listener);
+ }
+
+ /**
+ * Removes the given RowIdChangeListener from this query
+ */
+ @Override
+ public void removeListener(RowIdChangeListener listener) {
+ if (rowIdChangeListeners != null) {
+ rowIdChangeListeners.remove(listener);
+ }
+ }
+
+ private static final Logger getLogger() {
+ return Logger.getLogger(TableQuery.class.getName());
+ }
+}
diff --git a/server/src/com/vaadin/data/util/sqlcontainer/query/generator/DefaultSQLGenerator.java b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/DefaultSQLGenerator.java
new file mode 100644
index 0000000000..6485330541
--- /dev/null
+++ b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/DefaultSQLGenerator.java
@@ -0,0 +1,367 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util.sqlcontainer.query.generator;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import com.vaadin.data.Container.Filter;
+import com.vaadin.data.util.sqlcontainer.ColumnProperty;
+import com.vaadin.data.util.sqlcontainer.RowItem;
+import com.vaadin.data.util.sqlcontainer.SQLUtil;
+import com.vaadin.data.util.sqlcontainer.TemporaryRowId;
+import com.vaadin.data.util.sqlcontainer.query.OrderBy;
+import com.vaadin.data.util.sqlcontainer.query.generator.filter.QueryBuilder;
+import com.vaadin.data.util.sqlcontainer.query.generator.filter.StringDecorator;
+
+/**
+ * Generates generic SQL that is supported by HSQLDB, MySQL and PostgreSQL.
+ *
+ * @author Jonatan Kronqvist / Vaadin Ltd
+ */
+@SuppressWarnings("serial")
+public class DefaultSQLGenerator implements SQLGenerator {
+
+ private Class<? extends StatementHelper> statementHelperClass = null;
+
+ public DefaultSQLGenerator() {
+
+ }
+
+ /**
+ * Create a new DefaultSqlGenerator instance that uses the given
+ * implementation of {@link StatementHelper}
+ *
+ * @param statementHelper
+ */
+ public DefaultSQLGenerator(
+ Class<? extends StatementHelper> statementHelperClazz) {
+ this();
+ statementHelperClass = statementHelperClazz;
+ }
+
+ /**
+ * Construct a DefaultSQLGenerator with the specified identifiers for start
+ * and end of quoted strings. The identifiers may be different depending on
+ * the database engine and it's settings.
+ *
+ * @param quoteStart
+ * the identifier (character) denoting the start of a quoted
+ * string
+ * @param quoteEnd
+ * the identifier (character) denoting the end of a quoted string
+ */
+ public DefaultSQLGenerator(String quoteStart, String quoteEnd) {
+ QueryBuilder.setStringDecorator(new StringDecorator(quoteStart,
+ quoteEnd));
+ }
+
+ /**
+ * Same as {@link #DefaultSQLGenerator(String, String)} but with support for
+ * custom {@link StatementHelper} implementation.
+ *
+ * @param quoteStart
+ * @param quoteEnd
+ * @param statementHelperClazz
+ */
+ public DefaultSQLGenerator(String quoteStart, String quoteEnd,
+ Class<? extends StatementHelper> statementHelperClazz) {
+ this(quoteStart, quoteEnd);
+ statementHelperClass = statementHelperClazz;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.addon.sqlcontainer.query.generator.SQLGenerator#
+ * generateSelectQuery(java.lang.String, java.util.List, java.util.List,
+ * int, int, java.lang.String)
+ */
+ @Override
+ public StatementHelper generateSelectQuery(String tableName,
+ List<Filter> filters, List<OrderBy> orderBys, int offset,
+ int pagelength, String toSelect) {
+ if (tableName == null || tableName.trim().equals("")) {
+ throw new IllegalArgumentException("Table name must be given.");
+ }
+ toSelect = toSelect == null ? "*" : toSelect;
+ StatementHelper sh = getStatementHelper();
+ StringBuffer query = new StringBuffer();
+ query.append("SELECT " + toSelect + " FROM ").append(
+ SQLUtil.escapeSQL(tableName));
+ if (filters != null) {
+ query.append(QueryBuilder.getWhereStringForFilters(filters, sh));
+ }
+ if (orderBys != null) {
+ for (OrderBy o : orderBys) {
+ generateOrderBy(query, o, orderBys.indexOf(o) == 0);
+ }
+ }
+ if (pagelength != 0) {
+ generateLimits(query, offset, pagelength);
+ }
+ sh.setQueryString(query.toString());
+ return sh;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.addon.sqlcontainer.query.generator.SQLGenerator#
+ * generateUpdateQuery(java.lang.String,
+ * com.vaadin.addon.sqlcontainer.RowItem)
+ */
+ @Override
+ public StatementHelper generateUpdateQuery(String tableName, RowItem item) {
+ if (tableName == null || tableName.trim().equals("")) {
+ throw new IllegalArgumentException("Table name must be given.");
+ }
+ if (item == null) {
+ throw new IllegalArgumentException("Updated item must be given.");
+ }
+ StatementHelper sh = getStatementHelper();
+ StringBuffer query = new StringBuffer();
+ query.append("UPDATE ").append(tableName).append(" SET");
+
+ /* Generate column<->value and rowidentifiers map */
+ Map<String, Object> columnToValueMap = generateColumnToValueMap(item);
+ Map<String, Object> rowIdentifiers = generateRowIdentifiers(item);
+ /* Generate columns and values to update */
+ boolean first = true;
+ for (String column : columnToValueMap.keySet()) {
+ if (first) {
+ query.append(" " + QueryBuilder.quote(column) + " = ?");
+ } else {
+ query.append(", " + QueryBuilder.quote(column) + " = ?");
+ }
+ sh.addParameterValue(columnToValueMap.get(column), item
+ .getItemProperty(column).getType());
+ first = false;
+ }
+ /* Generate identifiers for the row to be updated */
+ first = true;
+ for (String column : rowIdentifiers.keySet()) {
+ if (first) {
+ query.append(" WHERE " + QueryBuilder.quote(column) + " = ?");
+ } else {
+ query.append(" AND " + QueryBuilder.quote(column) + " = ?");
+ }
+ sh.addParameterValue(rowIdentifiers.get(column), item
+ .getItemProperty(column).getType());
+ first = false;
+ }
+ sh.setQueryString(query.toString());
+ return sh;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.addon.sqlcontainer.query.generator.SQLGenerator#
+ * generateInsertQuery(java.lang.String,
+ * com.vaadin.addon.sqlcontainer.RowItem)
+ */
+ @Override
+ public StatementHelper generateInsertQuery(String tableName, RowItem item) {
+ if (tableName == null || tableName.trim().equals("")) {
+ throw new IllegalArgumentException("Table name must be given.");
+ }
+ if (item == null) {
+ throw new IllegalArgumentException("New item must be given.");
+ }
+ if (!(item.getId() instanceof TemporaryRowId)) {
+ throw new IllegalArgumentException(
+ "Cannot generate an insert query for item already in database.");
+ }
+ StatementHelper sh = getStatementHelper();
+ StringBuffer query = new StringBuffer();
+ query.append("INSERT INTO ").append(tableName).append(" (");
+
+ /* Generate column<->value map */
+ Map<String, Object> columnToValueMap = generateColumnToValueMap(item);
+ /* Generate column names for insert query */
+ boolean first = true;
+ for (String column : columnToValueMap.keySet()) {
+ if (!first) {
+ query.append(", ");
+ }
+ query.append(QueryBuilder.quote(column));
+ first = false;
+ }
+
+ /* Generate values for insert query */
+ query.append(") VALUES (");
+ first = true;
+ for (String column : columnToValueMap.keySet()) {
+ if (!first) {
+ query.append(", ");
+ }
+ query.append("?");
+ sh.addParameterValue(columnToValueMap.get(column), item
+ .getItemProperty(column).getType());
+ first = false;
+ }
+ query.append(")");
+ sh.setQueryString(query.toString());
+ return sh;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.addon.sqlcontainer.query.generator.SQLGenerator#
+ * generateDeleteQuery(java.lang.String,
+ * com.vaadin.addon.sqlcontainer.RowItem)
+ */
+ @Override
+ public StatementHelper generateDeleteQuery(String tableName,
+ List<String> primaryKeyColumns, String versionColumn, RowItem item) {
+ if (tableName == null || tableName.trim().equals("")) {
+ throw new IllegalArgumentException("Table name must be given.");
+ }
+ if (item == null) {
+ throw new IllegalArgumentException(
+ "Item to be deleted must be given.");
+ }
+ if (primaryKeyColumns == null || primaryKeyColumns.isEmpty()) {
+ throw new IllegalArgumentException(
+ "Valid keyColumnNames must be provided.");
+ }
+ StatementHelper sh = getStatementHelper();
+ StringBuffer query = new StringBuffer();
+ query.append("DELETE FROM ").append(tableName).append(" WHERE ");
+ int count = 1;
+ for (String keyColName : primaryKeyColumns) {
+ if ((this instanceof MSSQLGenerator || this instanceof OracleGenerator)
+ && keyColName.equalsIgnoreCase("rownum")) {
+ count++;
+ continue;
+ }
+ if (count > 1) {
+ query.append(" AND ");
+ }
+ if (item.getItemProperty(keyColName).getValue() != null) {
+ query.append(QueryBuilder.quote(keyColName) + " = ?");
+ sh.addParameterValue(item.getItemProperty(keyColName)
+ .getValue(), item.getItemProperty(keyColName).getType());
+ }
+ count++;
+ }
+ if (versionColumn != null) {
+ query.append(String.format(" AND %s = ?",
+ QueryBuilder.quote(versionColumn)));
+ sh.addParameterValue(
+ item.getItemProperty(versionColumn).getValue(), item
+ .getItemProperty(versionColumn).getType());
+ }
+
+ sh.setQueryString(query.toString());
+ return sh;
+ }
+
+ /**
+ * Generates sorting rules as an ORDER BY -clause
+ *
+ * @param sb
+ * StringBuffer to which the clause is appended.
+ * @param o
+ * OrderBy object to be added into the sb.
+ * @param firstOrderBy
+ * If true, this is the first OrderBy.
+ * @return
+ */
+ protected StringBuffer generateOrderBy(StringBuffer sb, OrderBy o,
+ boolean firstOrderBy) {
+ if (firstOrderBy) {
+ sb.append(" ORDER BY ");
+ } else {
+ sb.append(", ");
+ }
+ sb.append(QueryBuilder.quote(o.getColumn()));
+ if (o.isAscending()) {
+ sb.append(" ASC");
+ } else {
+ sb.append(" DESC");
+ }
+ return sb;
+ }
+
+ /**
+ * Generates the LIMIT and OFFSET clause.
+ *
+ * @param sb
+ * StringBuffer to which the clause is appended.
+ * @param offset
+ * Value for offset.
+ * @param pagelength
+ * Value for pagelength.
+ * @return StringBuffer with LIMIT and OFFSET clause added.
+ */
+ protected StringBuffer generateLimits(StringBuffer sb, int offset,
+ int pagelength) {
+ sb.append(" LIMIT ").append(pagelength).append(" OFFSET ")
+ .append(offset);
+ return sb;
+ }
+
+ protected Map<String, Object> generateColumnToValueMap(RowItem item) {
+ Map<String, Object> columnToValueMap = new HashMap<String, Object>();
+ for (Object id : item.getItemPropertyIds()) {
+ ColumnProperty cp = (ColumnProperty) item.getItemProperty(id);
+ /* Prevent "rownum" usage as a column name if MSSQL or ORACLE */
+ if ((this instanceof MSSQLGenerator || this instanceof OracleGenerator)
+ && cp.getPropertyId().equalsIgnoreCase("rownum")) {
+ continue;
+ }
+ Object value = cp.getValue() == null ? null : cp.getValue();
+ /* Only include properties whose read-only status can be altered */
+ if (cp.isReadOnlyChangeAllowed() && !cp.isVersionColumn()) {
+ columnToValueMap.put(cp.getPropertyId(), value);
+ }
+ }
+ return columnToValueMap;
+ }
+
+ protected Map<String, Object> generateRowIdentifiers(RowItem item) {
+ Map<String, Object> rowIdentifiers = new HashMap<String, Object>();
+ for (Object id : item.getItemPropertyIds()) {
+ ColumnProperty cp = (ColumnProperty) item.getItemProperty(id);
+ /* Prevent "rownum" usage as a column name if MSSQL or ORACLE */
+ if ((this instanceof MSSQLGenerator || this instanceof OracleGenerator)
+ && cp.getPropertyId().equalsIgnoreCase("rownum")) {
+ continue;
+ }
+ Object value = cp.getValue() == null ? null : cp.getValue();
+ if (!cp.isReadOnlyChangeAllowed() || cp.isVersionColumn()) {
+ rowIdentifiers.put(cp.getPropertyId(), value);
+ }
+ }
+ return rowIdentifiers;
+ }
+
+ /**
+ * Returns the statement helper for the generator. Override this to handle
+ * platform specific data types.
+ *
+ * @see http://dev.vaadin.com/ticket/9148
+ * @return a new instance of the statement helper
+ */
+ protected StatementHelper getStatementHelper() {
+ if (statementHelperClass == null) {
+ return new StatementHelper();
+ }
+
+ try {
+ return statementHelperClass.newInstance();
+ } catch (InstantiationException e) {
+ throw new RuntimeException(
+ "Unable to instantiate custom StatementHelper", e);
+ } catch (IllegalAccessException e) {
+ throw new RuntimeException(
+ "Unable to instantiate custom StatementHelper", e);
+ }
+ }
+
+}
diff --git a/server/src/com/vaadin/data/util/sqlcontainer/query/generator/MSSQLGenerator.java b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/MSSQLGenerator.java
new file mode 100644
index 0000000000..13ef1d0090
--- /dev/null
+++ b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/MSSQLGenerator.java
@@ -0,0 +1,101 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util.sqlcontainer.query.generator;
+
+import java.util.List;
+
+import com.vaadin.data.Container.Filter;
+import com.vaadin.data.util.sqlcontainer.query.OrderBy;
+import com.vaadin.data.util.sqlcontainer.query.generator.filter.QueryBuilder;
+
+@SuppressWarnings("serial")
+public class MSSQLGenerator extends DefaultSQLGenerator {
+
+ public MSSQLGenerator() {
+
+ }
+
+ /**
+ * Construct a MSSQLGenerator with the specified identifiers for start and
+ * end of quoted strings. The identifiers may be different depending on the
+ * database engine and it's settings.
+ *
+ * @param quoteStart
+ * the identifier (character) denoting the start of a quoted
+ * string
+ * @param quoteEnd
+ * the identifier (character) denoting the end of a quoted string
+ */
+ public MSSQLGenerator(String quoteStart, String quoteEnd) {
+ super(quoteStart, quoteEnd);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.addon.sqlcontainer.query.generator.DefaultSQLGenerator#
+ * generateSelectQuery(java.lang.String, java.util.List,
+ * com.vaadin.addon.sqlcontainer.query.FilteringMode, java.util.List, int,
+ * int, java.lang.String)
+ */
+ @Override
+ public StatementHelper generateSelectQuery(String tableName,
+ List<Filter> filters, List<OrderBy> orderBys, int offset,
+ int pagelength, String toSelect) {
+ if (tableName == null || tableName.trim().equals("")) {
+ throw new IllegalArgumentException("Table name must be given.");
+ }
+ /* Adjust offset and page length parameters to match "row numbers" */
+ offset = pagelength > 1 ? ++offset : offset;
+ pagelength = pagelength > 1 ? --pagelength : pagelength;
+ toSelect = toSelect == null ? "*" : toSelect;
+ StatementHelper sh = getStatementHelper();
+ StringBuffer query = new StringBuffer();
+
+ /* Row count request is handled here */
+ if ("COUNT(*)".equalsIgnoreCase(toSelect)) {
+ query.append(String.format(
+ "SELECT COUNT(*) AS %s FROM (SELECT * FROM %s",
+ QueryBuilder.quote("rowcount"), tableName));
+ if (filters != null && !filters.isEmpty()) {
+ query.append(QueryBuilder.getWhereStringForFilters(filters, sh));
+ }
+ query.append(") AS t");
+ sh.setQueryString(query.toString());
+ return sh;
+ }
+
+ /* SELECT without row number constraints */
+ if (offset == 0 && pagelength == 0) {
+ query.append("SELECT ").append(toSelect).append(" FROM ")
+ .append(tableName);
+ if (filters != null) {
+ query.append(QueryBuilder.getWhereStringForFilters(filters, sh));
+ }
+ if (orderBys != null) {
+ for (OrderBy o : orderBys) {
+ generateOrderBy(query, o, orderBys.indexOf(o) == 0);
+ }
+ }
+ sh.setQueryString(query.toString());
+ return sh;
+ }
+
+ /* Remaining SELECT cases are handled here */
+ query.append("SELECT * FROM (SELECT row_number() OVER (");
+ if (orderBys != null) {
+ for (OrderBy o : orderBys) {
+ generateOrderBy(query, o, orderBys.indexOf(o) == 0);
+ }
+ }
+ query.append(") AS rownum, " + toSelect + " FROM ").append(tableName);
+ if (filters != null) {
+ query.append(QueryBuilder.getWhereStringForFilters(filters, sh));
+ }
+ query.append(") AS a WHERE a.rownum BETWEEN ").append(offset)
+ .append(" AND ").append(Integer.toString(offset + pagelength));
+ sh.setQueryString(query.toString());
+ return sh;
+ }
+} \ No newline at end of file
diff --git a/server/src/com/vaadin/data/util/sqlcontainer/query/generator/OracleGenerator.java b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/OracleGenerator.java
new file mode 100644
index 0000000000..43a562d3a8
--- /dev/null
+++ b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/OracleGenerator.java
@@ -0,0 +1,112 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util.sqlcontainer.query.generator;
+
+import java.util.List;
+
+import com.vaadin.data.Container.Filter;
+import com.vaadin.data.util.sqlcontainer.query.OrderBy;
+import com.vaadin.data.util.sqlcontainer.query.generator.filter.QueryBuilder;
+
+@SuppressWarnings("serial")
+public class OracleGenerator extends DefaultSQLGenerator {
+
+ public OracleGenerator() {
+
+ }
+
+ public OracleGenerator(Class<? extends StatementHelper> statementHelperClazz) {
+ super(statementHelperClazz);
+ }
+
+ /**
+ * Construct an OracleSQLGenerator with the specified identifiers for start
+ * and end of quoted strings. The identifiers may be different depending on
+ * the database engine and it's settings.
+ *
+ * @param quoteStart
+ * the identifier (character) denoting the start of a quoted
+ * string
+ * @param quoteEnd
+ * the identifier (character) denoting the end of a quoted string
+ */
+ public OracleGenerator(String quoteStart, String quoteEnd) {
+ super(quoteStart, quoteEnd);
+ }
+
+ public OracleGenerator(String quoteStart, String quoteEnd,
+ Class<? extends StatementHelper> statementHelperClazz) {
+ super(quoteStart, quoteEnd, statementHelperClazz);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.addon.sqlcontainer.query.generator.DefaultSQLGenerator#
+ * generateSelectQuery(java.lang.String, java.util.List,
+ * com.vaadin.addon.sqlcontainer.query.FilteringMode, java.util.List, int,
+ * int, java.lang.String)
+ */
+ @Override
+ public StatementHelper generateSelectQuery(String tableName,
+ List<Filter> filters, List<OrderBy> orderBys, int offset,
+ int pagelength, String toSelect) {
+ if (tableName == null || tableName.trim().equals("")) {
+ throw new IllegalArgumentException("Table name must be given.");
+ }
+ /* Adjust offset and page length parameters to match "row numbers" */
+ offset = pagelength > 1 ? ++offset : offset;
+ pagelength = pagelength > 1 ? --pagelength : pagelength;
+ toSelect = toSelect == null ? "*" : toSelect;
+ StatementHelper sh = getStatementHelper();
+ StringBuffer query = new StringBuffer();
+
+ /* Row count request is handled here */
+ if ("COUNT(*)".equalsIgnoreCase(toSelect)) {
+ query.append(String.format(
+ "SELECT COUNT(*) AS %s FROM (SELECT * FROM %s",
+ QueryBuilder.quote("rowcount"), tableName));
+ if (filters != null && !filters.isEmpty()) {
+ query.append(QueryBuilder.getWhereStringForFilters(filters, sh));
+ }
+ query.append(")");
+ sh.setQueryString(query.toString());
+ return sh;
+ }
+
+ /* SELECT without row number constraints */
+ if (offset == 0 && pagelength == 0) {
+ query.append("SELECT ").append(toSelect).append(" FROM ")
+ .append(tableName);
+ if (filters != null) {
+ query.append(QueryBuilder.getWhereStringForFilters(filters, sh));
+ }
+ if (orderBys != null) {
+ for (OrderBy o : orderBys) {
+ generateOrderBy(query, o, orderBys.indexOf(o) == 0);
+ }
+ }
+ sh.setQueryString(query.toString());
+ return sh;
+ }
+
+ /* Remaining SELECT cases are handled here */
+ query.append(String
+ .format("SELECT * FROM (SELECT x.*, ROWNUM AS %s FROM (SELECT %s FROM %s",
+ QueryBuilder.quote("rownum"), toSelect, tableName));
+ if (filters != null) {
+ query.append(QueryBuilder.getWhereStringForFilters(filters, sh));
+ }
+ if (orderBys != null) {
+ for (OrderBy o : orderBys) {
+ generateOrderBy(query, o, orderBys.indexOf(o) == 0);
+ }
+ }
+ query.append(String.format(") x) WHERE %s BETWEEN %d AND %d",
+ QueryBuilder.quote("rownum"), offset, offset + pagelength));
+ sh.setQueryString(query.toString());
+ return sh;
+ }
+
+} \ No newline at end of file
diff --git a/server/src/com/vaadin/data/util/sqlcontainer/query/generator/SQLGenerator.java b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/SQLGenerator.java
new file mode 100644
index 0000000000..dde7077eee
--- /dev/null
+++ b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/SQLGenerator.java
@@ -0,0 +1,88 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util.sqlcontainer.query.generator;
+
+import java.io.Serializable;
+import java.util.List;
+
+import com.vaadin.data.Container.Filter;
+import com.vaadin.data.util.sqlcontainer.RowItem;
+import com.vaadin.data.util.sqlcontainer.query.OrderBy;
+
+/**
+ * The SQLGenerator interface is meant to be implemented for each different SQL
+ * syntax that is to be supported. By default there are implementations for
+ * HSQLDB, MySQL, PostgreSQL, MSSQL and Oracle syntaxes.
+ *
+ * @author Jonatan Kronqvist / Vaadin Ltd
+ */
+public interface SQLGenerator extends Serializable {
+ /**
+ * Generates a SELECT query with the provided parameters. Uses default
+ * filtering mode (INCLUSIVE).
+ *
+ * @param tableName
+ * Name of the table queried
+ * @param filters
+ * The filters, converted into a WHERE clause
+ * @param orderBys
+ * The the ordering conditions, converted into an ORDER BY clause
+ * @param offset
+ * The offset of the first row to be included
+ * @param pagelength
+ * The number of rows to be returned when the query executes
+ * @param toSelect
+ * String containing what to select, e.g. "*", "COUNT(*)"
+ * @return StatementHelper instance containing the query string for a
+ * PreparedStatement and the values required for the parameters
+ */
+ public StatementHelper generateSelectQuery(String tableName,
+ List<Filter> filters, List<OrderBy> orderBys, int offset,
+ int pagelength, String toSelect);
+
+ /**
+ * Generates an UPDATE query with the provided parameters.
+ *
+ * @param tableName
+ * Name of the table queried
+ * @param item
+ * RowItem containing the updated values update.
+ * @return StatementHelper instance containing the query string for a
+ * PreparedStatement and the values required for the parameters
+ */
+ public StatementHelper generateUpdateQuery(String tableName, RowItem item);
+
+ /**
+ * Generates an INSERT query for inserting a new row with the provided
+ * values.
+ *
+ * @param tableName
+ * Name of the table queried
+ * @param item
+ * New RowItem to be inserted into the database.
+ * @return StatementHelper instance containing the query string for a
+ * PreparedStatement and the values required for the parameters
+ */
+ public StatementHelper generateInsertQuery(String tableName, RowItem item);
+
+ /**
+ * Generates a DELETE query for deleting data related to the given RowItem
+ * from the database.
+ *
+ * @param tableName
+ * Name of the table queried
+ * @param primaryKeyColumns
+ * the names of the columns holding the primary key. Usually just
+ * one column, but might be several.
+ * @param versionColumn
+ * the column containing the version number of the row, null if
+ * versioning (optimistic locking) not enabled.
+ * @param item
+ * Item to be deleted from the database
+ * @return StatementHelper instance containing the query string for a
+ * PreparedStatement and the values required for the parameters
+ */
+ public StatementHelper generateDeleteQuery(String tableName,
+ List<String> primaryKeyColumns, String versionColumn, RowItem item);
+}
diff --git a/server/src/com/vaadin/data/util/sqlcontainer/query/generator/StatementHelper.java b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/StatementHelper.java
new file mode 100644
index 0000000000..b012ce7685
--- /dev/null
+++ b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/StatementHelper.java
@@ -0,0 +1,163 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util.sqlcontainer.query.generator;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.sql.Date;
+import java.sql.PreparedStatement;
+import java.sql.SQLException;
+import java.sql.Time;
+import java.sql.Timestamp;
+import java.sql.Types;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * StatementHelper is a simple helper class that assists TableQuery and the
+ * query generators in filling a PreparedStatement. The actual statement is
+ * generated by the query generator methods, but the resulting statement and all
+ * the parameter values are stored in an instance of StatementHelper.
+ *
+ * This class will also fill the values with correct setters into the
+ * PreparedStatement on request.
+ */
+public class StatementHelper implements Serializable {
+
+ private String queryString;
+
+ private List<Object> parameters = new ArrayList<Object>();
+ private Map<Integer, Class<?>> dataTypes = new HashMap<Integer, Class<?>>();
+
+ public StatementHelper() {
+ }
+
+ public void setQueryString(String queryString) {
+ this.queryString = queryString;
+ }
+
+ public String getQueryString() {
+ return queryString;
+ }
+
+ public void addParameterValue(Object parameter) {
+ if (parameter != null) {
+ parameters.add(parameter);
+ dataTypes.put(parameters.size() - 1, parameter.getClass());
+ } else {
+ throw new IllegalArgumentException(
+ "You cannot add null parameters using addParamaters(Object). "
+ + "Use addParameters(Object,Class) instead");
+ }
+ }
+
+ public void addParameterValue(Object parameter, Class<?> type) {
+ parameters.add(parameter);
+ dataTypes.put(parameters.size() - 1, type);
+ }
+
+ public void setParameterValuesToStatement(PreparedStatement pstmt)
+ throws SQLException {
+ for (int i = 0; i < parameters.size(); i++) {
+ if (parameters.get(i) == null) {
+ handleNullValue(i, pstmt);
+ } else {
+ pstmt.setObject(i + 1, parameters.get(i));
+ }
+ }
+
+ /*
+ * The following list contains the data types supported by
+ * PreparedStatement but not supported by SQLContainer:
+ *
+ * [The list is provided as PreparedStatement method signatures]
+ *
+ * setNCharacterStream(int parameterIndex, Reader value)
+ *
+ * setNClob(int parameterIndex, NClob value)
+ *
+ * setNString(int parameterIndex, String value)
+ *
+ * setRef(int parameterIndex, Ref x)
+ *
+ * setRowId(int parameterIndex, RowId x)
+ *
+ * setSQLXML(int parameterIndex, SQLXML xmlObject)
+ *
+ * setBytes(int parameterIndex, byte[] x)
+ *
+ * setCharacterStream(int parameterIndex, Reader reader)
+ *
+ * setClob(int parameterIndex, Clob x)
+ *
+ * setURL(int parameterIndex, URL x)
+ *
+ * setArray(int parameterIndex, Array x)
+ *
+ * setAsciiStream(int parameterIndex, InputStream x)
+ *
+ * setBinaryStream(int parameterIndex, InputStream x)
+ *
+ * setBlob(int parameterIndex, Blob x)
+ */
+ }
+
+ private void handleNullValue(int i, PreparedStatement pstmt)
+ throws SQLException {
+ if (BigDecimal.class.equals(dataTypes.get(i))) {
+ pstmt.setBigDecimal(i + 1, null);
+ } else if (Boolean.class.equals(dataTypes.get(i))) {
+ pstmt.setNull(i + 1, Types.BOOLEAN);
+ } else if (Byte.class.equals(dataTypes.get(i))) {
+ pstmt.setNull(i + 1, Types.SMALLINT);
+ } else if (Date.class.equals(dataTypes.get(i))) {
+ pstmt.setDate(i + 1, null);
+ } else if (Double.class.equals(dataTypes.get(i))) {
+ pstmt.setNull(i + 1, Types.DOUBLE);
+ } else if (Float.class.equals(dataTypes.get(i))) {
+ pstmt.setNull(i + 1, Types.FLOAT);
+ } else if (Integer.class.equals(dataTypes.get(i))) {
+ pstmt.setNull(i + 1, Types.INTEGER);
+ } else if (Long.class.equals(dataTypes.get(i))) {
+ pstmt.setNull(i + 1, Types.BIGINT);
+ } else if (Short.class.equals(dataTypes.get(i))) {
+ pstmt.setNull(i + 1, Types.SMALLINT);
+ } else if (String.class.equals(dataTypes.get(i))) {
+ pstmt.setString(i + 1, null);
+ } else if (Time.class.equals(dataTypes.get(i))) {
+ pstmt.setTime(i + 1, null);
+ } else if (Timestamp.class.equals(dataTypes.get(i))) {
+ pstmt.setTimestamp(i + 1, null);
+ } else {
+
+ if (handleUnrecognizedTypeNullValue(i, pstmt, dataTypes)) {
+ return;
+ }
+
+ throw new SQLException("Data type not supported by SQLContainer: "
+ + parameters.get(i).getClass().toString());
+ }
+ }
+
+ /**
+ * Handle unrecognized null values. Override this to handle null values for
+ * platform specific data types that are not handled by the default
+ * implementation of the {@link StatementHelper}.
+ *
+ * @param i
+ * @param pstmt
+ * @param dataTypes2
+ *
+ * @return true if handled, false otherwise
+ *
+ * @see {@link http://dev.vaadin.com/ticket/9148}
+ */
+ protected boolean handleUnrecognizedTypeNullValue(int i,
+ PreparedStatement pstmt, Map<Integer, Class<?>> dataTypes)
+ throws SQLException {
+ return false;
+ }
+}
diff --git a/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/AndTranslator.java b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/AndTranslator.java
new file mode 100644
index 0000000000..251a543a8a
--- /dev/null
+++ b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/AndTranslator.java
@@ -0,0 +1,23 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util.sqlcontainer.query.generator.filter;
+
+import com.vaadin.data.Container.Filter;
+import com.vaadin.data.util.filter.And;
+import com.vaadin.data.util.sqlcontainer.query.generator.StatementHelper;
+
+public class AndTranslator implements FilterTranslator {
+
+ @Override
+ public boolean translatesFilter(Filter filter) {
+ return filter instanceof And;
+ }
+
+ @Override
+ public String getWhereStringForFilter(Filter filter, StatementHelper sh) {
+ return QueryBuilder.group(QueryBuilder.getJoinedFilterString(
+ ((And) filter).getFilters(), "AND", sh));
+ }
+
+}
diff --git a/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/BetweenTranslator.java b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/BetweenTranslator.java
new file mode 100644
index 0000000000..4fcaf759ea
--- /dev/null
+++ b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/BetweenTranslator.java
@@ -0,0 +1,25 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util.sqlcontainer.query.generator.filter;
+
+import com.vaadin.data.Container.Filter;
+import com.vaadin.data.util.filter.Between;
+import com.vaadin.data.util.sqlcontainer.query.generator.StatementHelper;
+
+public class BetweenTranslator implements FilterTranslator {
+
+ @Override
+ public boolean translatesFilter(Filter filter) {
+ return filter instanceof Between;
+ }
+
+ @Override
+ public String getWhereStringForFilter(Filter filter, StatementHelper sh) {
+ Between between = (Between) filter;
+ sh.addParameterValue(between.getStartValue());
+ sh.addParameterValue(between.getEndValue());
+ return QueryBuilder.quote(between.getPropertyId()) + " BETWEEN ? AND ?";
+ }
+
+}
diff --git a/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/CompareTranslator.java b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/CompareTranslator.java
new file mode 100644
index 0000000000..4293e1d630
--- /dev/null
+++ b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/CompareTranslator.java
@@ -0,0 +1,38 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util.sqlcontainer.query.generator.filter;
+
+import com.vaadin.data.Container.Filter;
+import com.vaadin.data.util.filter.Compare;
+import com.vaadin.data.util.sqlcontainer.query.generator.StatementHelper;
+
+public class CompareTranslator implements FilterTranslator {
+
+ @Override
+ public boolean translatesFilter(Filter filter) {
+ return filter instanceof Compare;
+ }
+
+ @Override
+ public String getWhereStringForFilter(Filter filter, StatementHelper sh) {
+ Compare compare = (Compare) filter;
+ sh.addParameterValue(compare.getValue());
+ String prop = QueryBuilder.quote(compare.getPropertyId());
+ switch (compare.getOperation()) {
+ case EQUAL:
+ return prop + " = ?";
+ case GREATER:
+ return prop + " > ?";
+ case GREATER_OR_EQUAL:
+ return prop + " >= ?";
+ case LESS:
+ return prop + " < ?";
+ case LESS_OR_EQUAL:
+ return prop + " <= ?";
+ default:
+ return "";
+ }
+ }
+
+}
diff --git a/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/FilterTranslator.java b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/FilterTranslator.java
new file mode 100644
index 0000000000..84af9d5c97
--- /dev/null
+++ b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/FilterTranslator.java
@@ -0,0 +1,16 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util.sqlcontainer.query.generator.filter;
+
+import java.io.Serializable;
+
+import com.vaadin.data.Container.Filter;
+import com.vaadin.data.util.sqlcontainer.query.generator.StatementHelper;
+
+public interface FilterTranslator extends Serializable {
+ public boolean translatesFilter(Filter filter);
+
+ public String getWhereStringForFilter(Filter filter, StatementHelper sh);
+
+}
diff --git a/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/IsNullTranslator.java b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/IsNullTranslator.java
new file mode 100644
index 0000000000..a2a6cd2c09
--- /dev/null
+++ b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/IsNullTranslator.java
@@ -0,0 +1,22 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util.sqlcontainer.query.generator.filter;
+
+import com.vaadin.data.Container.Filter;
+import com.vaadin.data.util.filter.IsNull;
+import com.vaadin.data.util.sqlcontainer.query.generator.StatementHelper;
+
+public class IsNullTranslator implements FilterTranslator {
+
+ @Override
+ public boolean translatesFilter(Filter filter) {
+ return filter instanceof IsNull;
+ }
+
+ @Override
+ public String getWhereStringForFilter(Filter filter, StatementHelper sh) {
+ IsNull in = (IsNull) filter;
+ return QueryBuilder.quote(in.getPropertyId()) + " IS NULL";
+ }
+}
diff --git a/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/LikeTranslator.java b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/LikeTranslator.java
new file mode 100644
index 0000000000..25a85caec0
--- /dev/null
+++ b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/LikeTranslator.java
@@ -0,0 +1,30 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util.sqlcontainer.query.generator.filter;
+
+import com.vaadin.data.Container.Filter;
+import com.vaadin.data.util.filter.Like;
+import com.vaadin.data.util.sqlcontainer.query.generator.StatementHelper;
+
+public class LikeTranslator implements FilterTranslator {
+
+ @Override
+ public boolean translatesFilter(Filter filter) {
+ return filter instanceof Like;
+ }
+
+ @Override
+ public String getWhereStringForFilter(Filter filter, StatementHelper sh) {
+ Like like = (Like) filter;
+ if (like.isCaseSensitive()) {
+ sh.addParameterValue(like.getValue());
+ return QueryBuilder.quote(like.getPropertyId()) + " LIKE ?";
+ } else {
+ sh.addParameterValue(like.getValue().toUpperCase());
+ return "UPPER(" + QueryBuilder.quote(like.getPropertyId())
+ + ") LIKE ?";
+ }
+ }
+
+}
diff --git a/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/NotTranslator.java b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/NotTranslator.java
new file mode 100644
index 0000000000..5dfbe240e7
--- /dev/null
+++ b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/NotTranslator.java
@@ -0,0 +1,29 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util.sqlcontainer.query.generator.filter;
+
+import com.vaadin.data.Container.Filter;
+import com.vaadin.data.util.filter.IsNull;
+import com.vaadin.data.util.filter.Not;
+import com.vaadin.data.util.sqlcontainer.query.generator.StatementHelper;
+
+public class NotTranslator implements FilterTranslator {
+
+ @Override
+ public boolean translatesFilter(Filter filter) {
+ return filter instanceof Not;
+ }
+
+ @Override
+ public String getWhereStringForFilter(Filter filter, StatementHelper sh) {
+ Not not = (Not) filter;
+ if (not.getFilter() instanceof IsNull) {
+ IsNull in = (IsNull) not.getFilter();
+ return QueryBuilder.quote(in.getPropertyId()) + " IS NOT NULL";
+ }
+ return "NOT "
+ + QueryBuilder.getWhereStringForFilter(not.getFilter(), sh);
+ }
+
+}
diff --git a/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/OrTranslator.java b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/OrTranslator.java
new file mode 100644
index 0000000000..2f0ed814e0
--- /dev/null
+++ b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/OrTranslator.java
@@ -0,0 +1,23 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util.sqlcontainer.query.generator.filter;
+
+import com.vaadin.data.Container.Filter;
+import com.vaadin.data.util.filter.Or;
+import com.vaadin.data.util.sqlcontainer.query.generator.StatementHelper;
+
+public class OrTranslator implements FilterTranslator {
+
+ @Override
+ public boolean translatesFilter(Filter filter) {
+ return filter instanceof Or;
+ }
+
+ @Override
+ public String getWhereStringForFilter(Filter filter, StatementHelper sh) {
+ return QueryBuilder.group(QueryBuilder.getJoinedFilterString(
+ ((Or) filter).getFilters(), "OR", sh));
+ }
+
+}
diff --git a/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/QueryBuilder.java b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/QueryBuilder.java
new file mode 100644
index 0000000000..24be8963e0
--- /dev/null
+++ b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/QueryBuilder.java
@@ -0,0 +1,98 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util.sqlcontainer.query.generator.filter;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import com.vaadin.data.Container.Filter;
+import com.vaadin.data.util.sqlcontainer.query.generator.StatementHelper;
+
+public class QueryBuilder implements Serializable {
+
+ private static ArrayList<FilterTranslator> filterTranslators = new ArrayList<FilterTranslator>();
+ private static StringDecorator stringDecorator = new StringDecorator("\"",
+ "\"");
+
+ static {
+ /* Register all default filter translators */
+ addFilterTranslator(new AndTranslator());
+ addFilterTranslator(new OrTranslator());
+ addFilterTranslator(new LikeTranslator());
+ addFilterTranslator(new BetweenTranslator());
+ addFilterTranslator(new CompareTranslator());
+ addFilterTranslator(new NotTranslator());
+ addFilterTranslator(new IsNullTranslator());
+ addFilterTranslator(new SimpleStringTranslator());
+ }
+
+ public synchronized static void addFilterTranslator(
+ FilterTranslator translator) {
+ filterTranslators.add(translator);
+ }
+
+ /**
+ * Allows specification of a custom ColumnQuoter instance that handles
+ * quoting of column names for the current DB dialect.
+ *
+ * @param decorator
+ * the ColumnQuoter instance to use.
+ */
+ public static void setStringDecorator(StringDecorator decorator) {
+ stringDecorator = decorator;
+ }
+
+ public static String quote(Object str) {
+ return stringDecorator.quote(str);
+ }
+
+ public static String group(String str) {
+ return stringDecorator.group(str);
+ }
+
+ /**
+ * Constructs and returns a string representing the filter that can be used
+ * in a WHERE clause.
+ *
+ * @param filter
+ * the filter to translate
+ * @param sh
+ * the statement helper to update with the value(s) of the filter
+ * @return a string representing the filter.
+ */
+ public synchronized static String getWhereStringForFilter(Filter filter,
+ StatementHelper sh) {
+ for (FilterTranslator ft : filterTranslators) {
+ if (ft.translatesFilter(filter)) {
+ return ft.getWhereStringForFilter(filter, sh);
+ }
+ }
+ return "";
+ }
+
+ public static String getJoinedFilterString(Collection<Filter> filters,
+ String joinString, StatementHelper sh) {
+ StringBuilder result = new StringBuilder();
+ for (Filter f : filters) {
+ result.append(getWhereStringForFilter(f, sh));
+ result.append(" ").append(joinString).append(" ");
+ }
+ // Remove the last instance of joinString
+ result.delete(result.length() - joinString.length() - 2,
+ result.length());
+ return result.toString();
+ }
+
+ public static String getWhereStringForFilters(List<Filter> filters,
+ StatementHelper sh) {
+ if (filters == null || filters.isEmpty()) {
+ return "";
+ }
+ StringBuilder where = new StringBuilder(" WHERE ");
+ where.append(getJoinedFilterString(filters, "AND", sh));
+ return where.toString();
+ }
+}
diff --git a/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/SimpleStringTranslator.java b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/SimpleStringTranslator.java
new file mode 100644
index 0000000000..f108003535
--- /dev/null
+++ b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/SimpleStringTranslator.java
@@ -0,0 +1,30 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util.sqlcontainer.query.generator.filter;
+
+import com.vaadin.data.Container.Filter;
+import com.vaadin.data.util.filter.Like;
+import com.vaadin.data.util.filter.SimpleStringFilter;
+import com.vaadin.data.util.sqlcontainer.query.generator.StatementHelper;
+
+public class SimpleStringTranslator implements FilterTranslator {
+
+ @Override
+ public boolean translatesFilter(Filter filter) {
+ return filter instanceof SimpleStringFilter;
+ }
+
+ @Override
+ public String getWhereStringForFilter(Filter filter, StatementHelper sh) {
+ SimpleStringFilter ssf = (SimpleStringFilter) filter;
+ // Create a Like filter based on the SimpleStringFilter and execute the
+ // LikeTranslator
+ String likeStr = ssf.isOnlyMatchPrefix() ? ssf.getFilterString() + "%"
+ : "%" + ssf.getFilterString() + "%";
+ Like like = new Like(ssf.getPropertyId().toString(), likeStr);
+ like.setCaseSensitive(!ssf.isIgnoreCase());
+ return new LikeTranslator().getWhereStringForFilter(like, sh);
+ }
+
+}
diff --git a/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/StringDecorator.java b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/StringDecorator.java
new file mode 100644
index 0000000000..8d2eabb5bc
--- /dev/null
+++ b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/StringDecorator.java
@@ -0,0 +1,58 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util.sqlcontainer.query.generator.filter;
+
+import java.io.Serializable;
+
+/**
+ * The StringDecorator knows how to produce a quoted string using the specified
+ * quote start and quote end characters. It also handles grouping of a string
+ * (surrounding it in parenthesis).
+ *
+ * Extend this class if you need to support special characters for grouping
+ * (parenthesis).
+ *
+ * @author Vaadin Ltd
+ */
+public class StringDecorator implements Serializable {
+
+ private final String quoteStart;
+ private final String quoteEnd;
+
+ /**
+ * Constructs a StringDecorator that uses the quoteStart and quoteEnd
+ * characters to create quoted strings.
+ *
+ * @param quoteStart
+ * the character denoting the start of a quote.
+ * @param quoteEnd
+ * the character denoting the end of a quote.
+ */
+ public StringDecorator(String quoteStart, String quoteEnd) {
+ this.quoteStart = quoteStart;
+ this.quoteEnd = quoteEnd;
+ }
+
+ /**
+ * Surround a string with quote characters.
+ *
+ * @param str
+ * the string to quote
+ * @return the quoted string
+ */
+ public String quote(Object str) {
+ return quoteStart + str + quoteEnd;
+ }
+
+ /**
+ * Groups a string by surrounding it in parenthesis
+ *
+ * @param str
+ * the string to group
+ * @return the grouped string
+ */
+ public String group(String str) {
+ return "(" + str + ")";
+ }
+}
diff --git a/server/src/com/vaadin/data/validator/AbstractStringValidator.java b/server/src/com/vaadin/data/validator/AbstractStringValidator.java
new file mode 100644
index 0000000000..5267cc7b7b
--- /dev/null
+++ b/server/src/com/vaadin/data/validator/AbstractStringValidator.java
@@ -0,0 +1,42 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.validator;
+
+/**
+ * Validator base class for validating strings.
+ * <p>
+ * To include the value that failed validation in the exception message you can
+ * use "{0}" in the error message. This will be replaced with the failed value
+ * (converted to string using {@link #toString()}) or "null" if the value is
+ * null.
+ * </p>
+ *
+ * @author Vaadin Ltd.
+ * @version @VERSION@
+ * @since 5.4
+ */
+@SuppressWarnings("serial")
+public abstract class AbstractStringValidator extends AbstractValidator<String> {
+
+ /**
+ * Constructs a validator for strings.
+ *
+ * <p>
+ * Null and empty string values are always accepted. To reject empty values,
+ * set the field being validated as required.
+ * </p>
+ *
+ * @param errorMessage
+ * the message to be included in an {@link InvalidValueException}
+ * (with "{0}" replaced by the value that failed validation).
+ * */
+ public AbstractStringValidator(String errorMessage) {
+ super(errorMessage);
+ }
+
+ @Override
+ public Class<String> getType() {
+ return String.class;
+ }
+}
diff --git a/server/src/com/vaadin/data/validator/AbstractValidator.java b/server/src/com/vaadin/data/validator/AbstractValidator.java
new file mode 100644
index 0000000000..8febe5338a
--- /dev/null
+++ b/server/src/com/vaadin/data/validator/AbstractValidator.java
@@ -0,0 +1,139 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.validator;
+
+import com.vaadin.data.Validator;
+
+/**
+ * Abstract {@link com.vaadin.data.Validator Validator} implementation that
+ * provides a basic Validator implementation except the
+ * {@link #isValidValue(Object)} method.
+ * <p>
+ * To include the value that failed validation in the exception message you can
+ * use "{0}" in the error message. This will be replaced with the failed value
+ * (converted to string using {@link #toString()}) or "null" if the value is
+ * null.
+ * </p>
+ * <p>
+ * The default implementation of AbstractValidator does not support HTML in
+ * error messages. To enable HTML support, override
+ * {@link InvalidValueException#getHtmlMessage()} and throw such exceptions from
+ * {@link #validate(Object)}.
+ * </p>
+ * <p>
+ * Since Vaadin 7, subclasses can either implement {@link #validate(Object)}
+ * directly or implement {@link #isValidValue(Object)} when migrating legacy
+ * applications. To check validity, {@link #validate(Object)} should be used.
+ * </p>
+ *
+ * @param <T>
+ * The type
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 5.4
+ */
+public abstract class AbstractValidator<T> implements Validator {
+
+ /**
+ * Error message that is included in an {@link InvalidValueException} if
+ * such is thrown.
+ */
+ private String errorMessage;
+
+ /**
+ * Constructs a validator with the given error message.
+ *
+ * @param errorMessage
+ * the message to be included in an {@link InvalidValueException}
+ * (with "{0}" replaced by the value that failed validation).
+ */
+ public AbstractValidator(String errorMessage) {
+ this.errorMessage = errorMessage;
+ }
+
+ /**
+ * Since Vaadin 7, subclasses of AbstractValidator should override
+ * {@link #isValidValue(Object)} or {@link #validate(Object)} instead of
+ * {@link #isValid(Object)}. {@link #validate(Object)} should normally be
+ * used to check values.
+ *
+ * @param value
+ * @return true if the value is valid
+ */
+ public boolean isValid(Object value) {
+ try {
+ validate(value);
+ return true;
+ } catch (InvalidValueException e) {
+ return false;
+ }
+ }
+
+ /**
+ * Internally check the validity of a value. This method can be used to
+ * perform validation in subclasses if customization of the error message is
+ * not needed. Otherwise, subclasses should override
+ * {@link #validate(Object)} and the return value of this method is ignored.
+ *
+ * This method should not be called from outside the validator class itself.
+ *
+ * @param value
+ * @return
+ */
+ protected abstract boolean isValidValue(T value);
+
+ @Override
+ public void validate(Object value) throws InvalidValueException {
+ // isValidType ensures that value can safely be cast to TYPE
+ if (!isValidType(value) || !isValidValue((T) value)) {
+ String message = getErrorMessage().replace("{0}",
+ String.valueOf(value));
+ throw new InvalidValueException(message);
+ }
+ }
+
+ /**
+ * Checks the type of the value to validate to ensure it conforms with
+ * getType. Enables sub classes to handle the specific type instead of
+ * Object.
+ *
+ * @param value
+ * The value to check
+ * @return true if the value can safely be cast to the type specified by
+ * {@link #getType()}
+ */
+ protected boolean isValidType(Object value) {
+ if (value == null) {
+ return true;
+ }
+
+ return getType().isAssignableFrom(value.getClass());
+ }
+
+ /**
+ * Returns the message to be included in the exception in case the value
+ * does not validate.
+ *
+ * @return the error message provided in the constructor or using
+ * {@link #setErrorMessage(String)}.
+ */
+ public String getErrorMessage() {
+ return errorMessage;
+ }
+
+ /**
+ * Sets the message to be included in the exception in case the value does
+ * not validate. The exception message is typically shown to the end user.
+ *
+ * @param errorMessage
+ * the error message. "{0}" is automatically replaced by the
+ * value that did not validate.
+ */
+ public void setErrorMessage(String errorMessage) {
+ this.errorMessage = errorMessage;
+ }
+
+ public abstract Class<T> getType();
+}
diff --git a/server/src/com/vaadin/data/validator/BeanValidator.java b/server/src/com/vaadin/data/validator/BeanValidator.java
new file mode 100644
index 0000000000..816ff79b83
--- /dev/null
+++ b/server/src/com/vaadin/data/validator/BeanValidator.java
@@ -0,0 +1,176 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.data.validator;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+
+import javax.validation.ConstraintViolation;
+import javax.validation.MessageInterpolator.Context;
+import javax.validation.Validation;
+import javax.validation.ValidatorFactory;
+import javax.validation.metadata.ConstraintDescriptor;
+
+import com.vaadin.data.Validator;
+
+/**
+ * Vaadin {@link Validator} using the JSR-303 (javax.validation)
+ * annotation-based bean validation.
+ *
+ * The annotations of the fields of the beans are used to determine the
+ * validation to perform.
+ *
+ * Note that a JSR-303 implementation (e.g. Hibernate Validator or Apache Bean
+ * Validation - formerly agimatec validation) must be present on the project
+ * classpath when using bean validation.
+ *
+ * @since 7.0
+ *
+ * @author Petri Hakala
+ * @author Henri Sara
+ */
+public class BeanValidator implements Validator {
+
+ private static final long serialVersionUID = 1L;
+ private static ValidatorFactory factory;
+
+ private transient javax.validation.Validator javaxBeanValidator;
+ private String propertyName;
+ private Class<?> beanClass;
+ private Locale locale;
+
+ /**
+ * Simple implementation of a message interpolator context that returns
+ * fixed values.
+ */
+ protected static class SimpleContext implements Context, Serializable {
+
+ private final Object value;
+ private final ConstraintDescriptor<?> descriptor;
+
+ /**
+ * Create a simple immutable message interpolator context.
+ *
+ * @param value
+ * value being validated
+ * @param descriptor
+ * ConstraintDescriptor corresponding to the constraint being
+ * validated
+ */
+ public SimpleContext(Object value, ConstraintDescriptor<?> descriptor) {
+ this.value = value;
+ this.descriptor = descriptor;
+ }
+
+ @Override
+ public ConstraintDescriptor<?> getConstraintDescriptor() {
+ return descriptor;
+ }
+
+ @Override
+ public Object getValidatedValue() {
+ return value;
+ }
+
+ }
+
+ /**
+ * Creates a Vaadin {@link Validator} utilizing JSR-303 bean validation.
+ *
+ * @param beanClass
+ * bean class based on which the validation should be performed
+ * @param propertyName
+ * property to validate
+ */
+ public BeanValidator(Class<?> beanClass, String propertyName) {
+ this.beanClass = beanClass;
+ this.propertyName = propertyName;
+ locale = Locale.getDefault();
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Validator#validate(java.lang.Object)
+ */
+ @Override
+ public void validate(final Object value) throws InvalidValueException {
+ Set<?> violations = getJavaxBeanValidator().validateValue(beanClass,
+ propertyName, value);
+ if (violations.size() > 0) {
+ List<String> exceptions = new ArrayList<String>();
+ for (Object v : violations) {
+ final ConstraintViolation<?> violation = (ConstraintViolation<?>) v;
+ String msg = getJavaxBeanValidatorFactory()
+ .getMessageInterpolator().interpolate(
+ violation.getMessageTemplate(),
+ new SimpleContext(value, violation
+ .getConstraintDescriptor()), locale);
+ exceptions.add(msg);
+ }
+ StringBuilder b = new StringBuilder();
+ for (int i = 0; i < exceptions.size(); i++) {
+ if (i != 0) {
+ b.append("<br/>");
+ }
+ b.append(exceptions.get(i));
+ }
+ throw new InvalidValueException(b.toString());
+ }
+ }
+
+ /**
+ * Sets the locale used for validation error messages.
+ *
+ * Revalidation is not automatically triggered by setting the locale.
+ *
+ * @param locale
+ */
+ public void setLocale(Locale locale) {
+ this.locale = locale;
+ }
+
+ /**
+ * Gets the locale used for validation error messages.
+ *
+ * @return locale used for validation
+ */
+ public Locale getLocale() {
+ return locale;
+ }
+
+ /**
+ * Returns the underlying JSR-303 bean validator factory used. A factory is
+ * created using {@link Validation} if necessary.
+ *
+ * @return {@link ValidatorFactory} to use
+ */
+ protected static ValidatorFactory getJavaxBeanValidatorFactory() {
+ if (factory == null) {
+ factory = Validation.buildDefaultValidatorFactory();
+ }
+
+ return factory;
+ }
+
+ /**
+ * Returns a shared Validator instance to use. An instance is created using
+ * the validator factory if necessary and thereafter reused by the
+ * {@link BeanValidator} instance.
+ *
+ * @return the JSR-303 {@link javax.validation.Validator} to use
+ */
+ protected javax.validation.Validator getJavaxBeanValidator() {
+ if (javaxBeanValidator == null) {
+ javaxBeanValidator = getJavaxBeanValidatorFactory().getValidator();
+ }
+
+ return javaxBeanValidator;
+ }
+
+} \ No newline at end of file
diff --git a/server/src/com/vaadin/data/validator/CompositeValidator.java b/server/src/com/vaadin/data/validator/CompositeValidator.java
new file mode 100644
index 0000000000..cad31c9d4d
--- /dev/null
+++ b/server/src/com/vaadin/data/validator/CompositeValidator.java
@@ -0,0 +1,259 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.data.validator;
+
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+
+import com.vaadin.data.Validator;
+
+/**
+ * The <code>CompositeValidator</code> allows you to chain (compose) many
+ * validators to validate one field. The contained validators may be required to
+ * all validate the value to validate or it may be enough that one contained
+ * validator validates the value. This behaviour is controlled by the modes
+ * <code>AND</code> and <code>OR</code>.
+ *
+ * @author Vaadin Ltd.
+ * @version @VERSION@
+ * @since 3.0
+ */
+@SuppressWarnings("serial")
+public class CompositeValidator implements Validator {
+
+ public enum CombinationMode {
+ /**
+ * The validators are combined with <code>AND</code> clause: validity of
+ * the composite implies validity of the all validators it is composed
+ * of must be valid.
+ */
+ AND,
+ /**
+ * The validators are combined with <code>OR</code> clause: validity of
+ * the composite implies that some of validators it is composed of must
+ * be valid.
+ */
+ OR;
+ }
+
+ /**
+ * @deprecated from 7.0, use {@link CombinationMode#AND} instead    
+ */
+ @Deprecated
+ public static final CombinationMode MODE_AND = CombinationMode.AND;
+ /**
+ * @deprecated from 7.0, use {@link CombinationMode#OR} instead    
+ */
+ @Deprecated
+ public static final CombinationMode MODE_OR = CombinationMode.OR;
+
+ private String errorMessage;
+
+ /**
+ * Operation mode.
+ */
+ private CombinationMode mode = CombinationMode.AND;
+
+ /**
+ * List of contained validators.
+ */
+ private final List<Validator> validators = new LinkedList<Validator>();
+
+ /**
+ * Construct a composite validator in <code>AND</code> mode without error
+ * message.
+ */
+ public CompositeValidator() {
+ this(CombinationMode.AND, "");
+ }
+
+ /**
+ * Constructs a composite validator in given mode.
+ *
+ * @param mode
+ * @param errorMessage
+ */
+ public CompositeValidator(CombinationMode mode, String errorMessage) {
+ setErrorMessage(errorMessage);
+ setMode(mode);
+ }
+
+ /**
+ * Validates the given value.
+ * <p>
+ * The value is valid, if:
+ * <ul>
+ * <li><code>MODE_AND</code>: All of the sub-validators are valid
+ * <li><code>MODE_OR</code>: Any of the sub-validators are valid
+ * </ul>
+ *
+ * If the value is invalid, validation error is thrown. If the error message
+ * is set (non-null), it is used. If the error message has not been set, the
+ * first error occurred is thrown.
+ * </p>
+ *
+ * @param value
+ * the value to check.
+ * @throws Validator.InvalidValueException
+ * if the value is not valid.
+ */
+ @Override
+ public void validate(Object value) throws Validator.InvalidValueException {
+ switch (mode) {
+ case AND:
+ for (Validator validator : validators) {
+ validator.validate(value);
+ }
+ return;
+
+ case OR:
+ Validator.InvalidValueException first = null;
+ for (Validator v : validators) {
+ try {
+ v.validate(value);
+ return;
+ } catch (final Validator.InvalidValueException e) {
+ if (first == null) {
+ first = e;
+ }
+ }
+ }
+ if (first == null) {
+ return;
+ }
+ final String em = getErrorMessage();
+ if (em != null) {
+ throw new Validator.InvalidValueException(em);
+ } else {
+ throw first;
+ }
+ }
+ }
+
+ /**
+ * Gets the mode of the validator.
+ *
+ * @return Operation mode of the validator: {@link CombinationMode#AND} or
+ * {@link CombinationMode#OR}.
+ */
+ public final CombinationMode getMode() {
+ return mode;
+ }
+
+ /**
+ * Sets the mode of the validator. The valid modes are:
+ * <ul>
+ * <li>{@link CombinationMode#AND} (default)
+ * <li>{@link CombinationMode#OR}
+ * </ul>
+ *
+ * @param mode
+ * the mode to set.
+ */
+ public void setMode(CombinationMode mode) {
+ if (mode == null) {
+ throw new IllegalArgumentException(
+ "The validator can't be set to null");
+ }
+ this.mode = mode;
+ }
+
+ /**
+ * Gets the error message for the composite validator. If the error message
+ * is null, original error messages of the sub-validators are used instead.
+ */
+ public String getErrorMessage() {
+ if (errorMessage != null) {
+ return errorMessage;
+ }
+
+ // TODO Return composite error message
+
+ return null;
+ }
+
+ /**
+ * Adds validator to the interface.
+ *
+ * @param validator
+ * the Validator object which performs validation checks on this
+ * set of data field values.
+ */
+ public void addValidator(Validator validator) {
+ if (validator == null) {
+ return;
+ }
+ validators.add(validator);
+ }
+
+ /**
+ * Removes a validator from the composite.
+ *
+ * @param validator
+ * the Validator object which performs validation checks on this
+ * set of data field values.
+ */
+ public void removeValidator(Validator validator) {
+ validators.remove(validator);
+ }
+
+ /**
+ * Gets sub-validators by class.
+ *
+ * <p>
+ * If the component contains directly or recursively (it contains another
+ * composite containing the validator) validators compatible with given type
+ * they are returned. This only applies to <code>AND</code> mode composite
+ * validators.
+ * </p>
+ *
+ * <p>
+ * If the validator is in <code>OR</code> mode or does not contain any
+ * validators of given type null is returned.
+ * </p>
+ *
+ * @param validatorType
+ * The type of validators to return
+ *
+ * @return Collection<Validator> of validators compatible with given type
+ * that must apply or null if none found.
+ */
+ public Collection<Validator> getSubValidators(Class validatorType) {
+ if (mode != CombinationMode.AND) {
+ return null;
+ }
+
+ final HashSet<Validator> found = new HashSet<Validator>();
+ for (Validator v : validators) {
+ if (validatorType.isAssignableFrom(v.getClass())) {
+ found.add(v);
+ }
+ if (v instanceof CompositeValidator
+ && ((CompositeValidator) v).getMode() == MODE_AND) {
+ final Collection<Validator> c = ((CompositeValidator) v)
+ .getSubValidators(validatorType);
+ if (c != null) {
+ found.addAll(c);
+ }
+ }
+ }
+
+ return found.isEmpty() ? null : found;
+ }
+
+ /**
+ * Sets the message to be included in the exception in case the value does
+ * not validate. The exception message is typically shown to the end user.
+ *
+ * @param errorMessage
+ * the error message.
+ */
+ public void setErrorMessage(String errorMessage) {
+ this.errorMessage = errorMessage;
+ }
+
+}
diff --git a/server/src/com/vaadin/data/validator/DateRangeValidator.java b/server/src/com/vaadin/data/validator/DateRangeValidator.java
new file mode 100644
index 0000000000..24f3d3ce10
--- /dev/null
+++ b/server/src/com/vaadin/data/validator/DateRangeValidator.java
@@ -0,0 +1,51 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.validator;
+
+import java.util.Date;
+
+import com.vaadin.ui.DateField.Resolution;
+
+/**
+ * Validator for validating that a Date is inside a given range.
+ *
+ * <p>
+ * Note that the comparison is done directly on the Date object so take care
+ * that the hours/minutes/seconds/milliseconds of the min/max values are
+ * properly set.
+ * </p>
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 7.0
+ */
+public class DateRangeValidator extends RangeValidator<Date> {
+
+ /**
+ * Creates a validator for checking that an Date is within a given range.
+ * <p>
+ * By default the range is inclusive i.e. both minValue and maxValue are
+ * valid values. Use {@link #setMinValueIncluded(boolean)} or
+ * {@link #setMaxValueIncluded(boolean)} to change it.
+ * </p>
+ * <p>
+ * Note that the comparison is done directly on the Date object so take care
+ * that the hours/minutes/seconds/milliseconds of the min/max values are
+ * properly set.
+ * </p>
+ *
+ * @param errorMessage
+ * the message to display in case the value does not validate.
+ * @param minValue
+ * The minimum value to accept or null for no limit
+ * @param maxValue
+ * The maximum value to accept or null for no limit
+ */
+ public DateRangeValidator(String errorMessage, Date minValue,
+ Date maxValue, Resolution resolution) {
+ super(errorMessage, Date.class, minValue, maxValue);
+ }
+
+}
diff --git a/server/src/com/vaadin/data/validator/DoubleRangeValidator.java b/server/src/com/vaadin/data/validator/DoubleRangeValidator.java
new file mode 100644
index 0000000000..05ae2f827e
--- /dev/null
+++ b/server/src/com/vaadin/data/validator/DoubleRangeValidator.java
@@ -0,0 +1,37 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.validator;
+
+/**
+ * Validator for validating that a {@link Double} is inside a given range.
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 7.0
+ */
+@SuppressWarnings("serial")
+public class DoubleRangeValidator extends RangeValidator<Double> {
+
+ /**
+ * Creates a validator for checking that an Double is within a given range.
+ *
+ * By default the range is inclusive i.e. both minValue and maxValue are
+ * valid values. Use {@link #setMinValueIncluded(boolean)} or
+ * {@link #setMaxValueIncluded(boolean)} to change it.
+ *
+ *
+ * @param errorMessage
+ * the message to display in case the value does not validate.
+ * @param minValue
+ * The minimum value to accept or null for no limit
+ * @param maxValue
+ * The maximum value to accept or null for no limit
+ */
+ public DoubleRangeValidator(String errorMessage, Double minValue,
+ Double maxValue) {
+ super(errorMessage, Double.class, minValue, maxValue);
+ }
+
+}
diff --git a/server/src/com/vaadin/data/validator/DoubleValidator.java b/server/src/com/vaadin/data/validator/DoubleValidator.java
new file mode 100644
index 0000000000..18f1909add
--- /dev/null
+++ b/server/src/com/vaadin/data/validator/DoubleValidator.java
@@ -0,0 +1,58 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.validator;
+
+/**
+ * String validator for a double precision floating point number. See
+ * {@link com.vaadin.data.validator.AbstractStringValidator} for more
+ * information.
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 5.4
+ * @deprecated in Vaadin 7.0. Use an Double converter on the field instead.
+ */
+@Deprecated
+@SuppressWarnings("serial")
+public class DoubleValidator extends AbstractStringValidator {
+
+ /**
+ * Creates a validator for checking that a string can be parsed as an
+ * double.
+ *
+ * @param errorMessage
+ * the message to display in case the value does not validate.
+ * @deprecated in Vaadin 7.0. Use a Double converter on the field instead
+ * and/or use a {@link DoubleRangeValidator} for validating that
+ * the value is inside a given range.
+ */
+ @Deprecated
+ public DoubleValidator(String errorMessage) {
+ super(errorMessage);
+ }
+
+ @Override
+ protected boolean isValidValue(String value) {
+ try {
+ Double.parseDouble(value);
+ return true;
+ } catch (Exception e) {
+ return false;
+ }
+ }
+
+ @Override
+ public void validate(Object value) throws InvalidValueException {
+ if (value != null && value instanceof Double) {
+ // Allow Doubles to pass through the validator for easier
+ // migration. Otherwise a TextField connected to an double property
+ // with a DoubleValidator will fail.
+ return;
+ }
+
+ super.validate(value);
+ }
+
+}
diff --git a/server/src/com/vaadin/data/validator/EmailValidator.java b/server/src/com/vaadin/data/validator/EmailValidator.java
new file mode 100644
index 0000000000..c76d7e13dc
--- /dev/null
+++ b/server/src/com/vaadin/data/validator/EmailValidator.java
@@ -0,0 +1,35 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.validator;
+
+/**
+ * String validator for e-mail addresses. The e-mail address syntax is not
+ * complete according to RFC 822 but handles the vast majority of valid e-mail
+ * addresses correctly.
+ *
+ * See {@link com.vaadin.data.validator.AbstractStringValidator} for more
+ * information.
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 5.4
+ */
+@SuppressWarnings("serial")
+public class EmailValidator extends RegexpValidator {
+
+ /**
+ * Creates a validator for checking that a string is a syntactically valid
+ * e-mail address.
+ *
+ * @param errorMessage
+ * the message to display in case the value does not validate.
+ */
+ public EmailValidator(String errorMessage) {
+ super(
+ "^([a-zA-Z0-9_\\.\\-+])+@(([a-zA-Z0-9-])+\\.)+([a-zA-Z0-9]{2,4})+$",
+ true, errorMessage);
+ }
+
+}
diff --git a/server/src/com/vaadin/data/validator/IntegerRangeValidator.java b/server/src/com/vaadin/data/validator/IntegerRangeValidator.java
new file mode 100644
index 0000000000..c171dd97d8
--- /dev/null
+++ b/server/src/com/vaadin/data/validator/IntegerRangeValidator.java
@@ -0,0 +1,37 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.validator;
+
+/**
+ * Validator for validating that an {@link Integer} is inside a given range.
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 5.4
+ */
+@SuppressWarnings("serial")
+public class IntegerRangeValidator extends RangeValidator<Integer> {
+
+ /**
+ * Creates a validator for checking that an Integer is within a given range.
+ *
+ * By default the range is inclusive i.e. both minValue and maxValue are
+ * valid values. Use {@link #setMinValueIncluded(boolean)} or
+ * {@link #setMaxValueIncluded(boolean)} to change it.
+ *
+ *
+ * @param errorMessage
+ * the message to display in case the value does not validate.
+ * @param minValue
+ * The minimum value to accept or null for no limit
+ * @param maxValue
+ * The maximum value to accept or null for no limit
+ */
+ public IntegerRangeValidator(String errorMessage, Integer minValue,
+ Integer maxValue) {
+ super(errorMessage, Integer.class, minValue, maxValue);
+ }
+
+}
diff --git a/server/src/com/vaadin/data/validator/IntegerValidator.java b/server/src/com/vaadin/data/validator/IntegerValidator.java
new file mode 100644
index 0000000000..88ae9f3f0b
--- /dev/null
+++ b/server/src/com/vaadin/data/validator/IntegerValidator.java
@@ -0,0 +1,58 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.validator;
+
+/**
+ * String validator for integers. See
+ * {@link com.vaadin.data.validator.AbstractStringValidator} for more
+ * information.
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 5.4
+ * @deprecated in Vaadin 7.0. Use an Integer converter on the field instead.
+ */
+@SuppressWarnings("serial")
+@Deprecated
+public class IntegerValidator extends AbstractStringValidator {
+
+ /**
+ * Creates a validator for checking that a string can be parsed as an
+ * integer.
+ *
+ * @param errorMessage
+ * the message to display in case the value does not validate.
+ * @deprecated in Vaadin 7.0. Use an Integer converter on the field instead
+ * and/or use an {@link IntegerRangeValidator} for validating
+ * that the value is inside a given range.
+ */
+ @Deprecated
+ public IntegerValidator(String errorMessage) {
+ super(errorMessage);
+
+ }
+
+ @Override
+ protected boolean isValidValue(String value) {
+ try {
+ Integer.parseInt(value);
+ return true;
+ } catch (Exception e) {
+ return false;
+ }
+ }
+
+ @Override
+ public void validate(Object value) throws InvalidValueException {
+ if (value != null && value instanceof Integer) {
+ // Allow Integers to pass through the validator for easier
+ // migration. Otherwise a TextField connected to an integer property
+ // with an IntegerValidator will fail.
+ return;
+ }
+
+ super.validate(value);
+ }
+}
diff --git a/server/src/com/vaadin/data/validator/NullValidator.java b/server/src/com/vaadin/data/validator/NullValidator.java
new file mode 100644
index 0000000000..551d88c776
--- /dev/null
+++ b/server/src/com/vaadin/data/validator/NullValidator.java
@@ -0,0 +1,92 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.data.validator;
+
+import com.vaadin.data.Validator;
+
+/**
+ * This validator is used for validating properties that do or do not allow null
+ * values. By default, nulls are not allowed.
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+@SuppressWarnings("serial")
+public class NullValidator implements Validator {
+
+ private boolean onlyNullAllowed;
+
+ private String errorMessage;
+
+ /**
+ * Creates a new NullValidator.
+ *
+ * @param errorMessage
+ * the error message to display on invalidation.
+ * @param onlyNullAllowed
+ * Are only nulls allowed?
+ */
+ public NullValidator(String errorMessage, boolean onlyNullAllowed) {
+ setErrorMessage(errorMessage);
+ setNullAllowed(onlyNullAllowed);
+ }
+
+ /**
+ * Validates the data given in value.
+ *
+ * @param value
+ * the value to validate.
+ * @throws Validator.InvalidValueException
+ * if the value was invalid.
+ */
+ @Override
+ public void validate(Object value) throws Validator.InvalidValueException {
+ if ((onlyNullAllowed && value != null)
+ || (!onlyNullAllowed && value == null)) {
+ throw new Validator.InvalidValueException(errorMessage);
+ }
+ }
+
+ /**
+ * Returns <code>true</code> if nulls are allowed otherwise
+ * <code>false</code>.
+ */
+ public final boolean isNullAllowed() {
+ return onlyNullAllowed;
+ }
+
+ /**
+ * Sets if nulls (and only nulls) are to be allowed.
+ *
+ * @param onlyNullAllowed
+ * If true, only nulls are allowed. If false only non-nulls are
+ * allowed. Do we allow nulls?
+ */
+ public void setNullAllowed(boolean onlyNullAllowed) {
+ this.onlyNullAllowed = onlyNullAllowed;
+ }
+
+ /**
+ * Gets the error message that is displayed in case the value is invalid.
+ *
+ * @return the Error Message.
+ */
+ public String getErrorMessage() {
+ return errorMessage;
+ }
+
+ /**
+ * Sets the error message to be displayed on invalid value.
+ *
+ * @param errorMessage
+ * the Error Message to set.
+ */
+ public void setErrorMessage(String errorMessage) {
+ this.errorMessage = errorMessage;
+ }
+
+}
diff --git a/server/src/com/vaadin/data/validator/RangeValidator.java b/server/src/com/vaadin/data/validator/RangeValidator.java
new file mode 100644
index 0000000000..433271274f
--- /dev/null
+++ b/server/src/com/vaadin/data/validator/RangeValidator.java
@@ -0,0 +1,186 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.validator;
+
+/**
+ * An base implementation for validating any objects that implement
+ * {@link Comparable}.
+ *
+ * Verifies that the value is of the given type and within the (optionally)
+ * given limits. Typically you want to use a sub class of this like
+ * {@link IntegerRangeValidator}, {@link DoubleRangeValidator} or
+ * {@link DateRangeValidator} in applications.
+ * <p>
+ * Note that {@link RangeValidator} always accept null values. Make a field
+ * required to ensure that no empty values are accepted or override
+ * {@link #isValidValue(Comparable)}.
+ * </p>
+ *
+ * @param <T>
+ * The type of Number to validate. Must implement Comparable so that
+ * minimum and maximum checks work.
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 7.0
+ */
+public class RangeValidator<T extends Comparable> extends AbstractValidator<T> {
+
+ private T minValue = null;
+ private boolean minValueIncluded = true;
+ private T maxValue = null;
+ private boolean maxValueIncluded = true;
+ private Class<T> type;
+
+ /**
+ * Creates a new range validator of the given type.
+ *
+ * @param errorMessage
+ * The error message to use if validation fails
+ * @param type
+ * The type of object the validator can validate.
+ * @param minValue
+ * The minimum value that should be accepted or null for no limit
+ * @param maxValue
+ * The maximum value that should be accepted or null for no limit
+ */
+ public RangeValidator(String errorMessage, Class<T> type, T minValue,
+ T maxValue) {
+ super(errorMessage);
+ this.type = type;
+ this.minValue = minValue;
+ this.maxValue = maxValue;
+ }
+
+ /**
+ * Checks if the minimum value is part of the accepted range
+ *
+ * @return true if the minimum value is part of the range, false otherwise
+ */
+ public boolean isMinValueIncluded() {
+ return minValueIncluded;
+ }
+
+ /**
+ * Sets if the minimum value is part of the accepted range
+ *
+ * @param minValueIncluded
+ * true if the minimum value should be part of the range, false
+ * otherwise
+ */
+ public void setMinValueIncluded(boolean minValueIncluded) {
+ this.minValueIncluded = minValueIncluded;
+ }
+
+ /**
+ * Checks if the maximum value is part of the accepted range
+ *
+ * @return true if the maximum value is part of the range, false otherwise
+ */
+ public boolean isMaxValueIncluded() {
+ return maxValueIncluded;
+ }
+
+ /**
+ * Sets if the maximum value is part of the accepted range
+ *
+ * @param maxValueIncluded
+ * true if the maximum value should be part of the range, false
+ * otherwise
+ */
+ public void setMaxValueIncluded(boolean maxValueIncluded) {
+ this.maxValueIncluded = maxValueIncluded;
+ }
+
+ /**
+ * Gets the minimum value of the range
+ *
+ * @return the minimum value
+ */
+ public T getMinValue() {
+ return minValue;
+ }
+
+ /**
+ * Sets the minimum value of the range. Use
+ * {@link #setMinValueIncluded(boolean)} to control whether this value is
+ * part of the range or not.
+ *
+ * @param minValue
+ * the minimum value
+ */
+ public void setMinValue(T minValue) {
+ this.minValue = minValue;
+ }
+
+ /**
+ * Gets the maximum value of the range
+ *
+ * @return the maximum value
+ */
+ public T getMaxValue() {
+ return maxValue;
+ }
+
+ /**
+ * Sets the maximum value of the range. Use
+ * {@link #setMaxValueIncluded(boolean)} to control whether this value is
+ * part of the range or not.
+ *
+ * @param maxValue
+ * the maximum value
+ */
+ public void setMaxValue(T maxValue) {
+ this.maxValue = maxValue;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.data.validator.AbstractValidator#isValidValue(java.lang.Object
+ * )
+ */
+ @Override
+ protected boolean isValidValue(T value) {
+ if (value == null) {
+ return true;
+ }
+
+ if (getMinValue() != null) {
+ // Ensure that the min limit is ok
+ int result = value.compareTo(getMinValue());
+ if (result < 0) {
+ // value less than min value
+ return false;
+ } else if (result == 0 && !isMinValueIncluded()) {
+ // values equal and min value not included
+ return false;
+ }
+ }
+ if (getMaxValue() != null) {
+ // Ensure that the Max limit is ok
+ int result = value.compareTo(getMaxValue());
+ if (result > 0) {
+ // value greater than max value
+ return false;
+ } else if (result == 0 && !isMaxValueIncluded()) {
+ // values equal and max value not included
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.validator.AbstractValidator#getType()
+ */
+ @Override
+ public Class<T> getType() {
+ return type;
+ }
+
+}
diff --git a/server/src/com/vaadin/data/validator/RegexpValidator.java b/server/src/com/vaadin/data/validator/RegexpValidator.java
new file mode 100644
index 0000000000..8143d54c97
--- /dev/null
+++ b/server/src/com/vaadin/data/validator/RegexpValidator.java
@@ -0,0 +1,97 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.validator;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * String validator comparing the string against a Java regular expression. Both
+ * complete matches and substring matches are supported.
+ *
+ * <p>
+ * For the Java regular expression syntax, see
+ * {@link java.util.regex.Pattern#sum}
+ * </p>
+ * <p>
+ * See {@link com.vaadin.data.validator.AbstractStringValidator} for more
+ * information.
+ * </p>
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 5.4
+ */
+@SuppressWarnings("serial")
+public class RegexpValidator extends AbstractStringValidator {
+
+ private Pattern pattern;
+ private boolean complete;
+ private transient Matcher matcher = null;
+
+ /**
+ * Creates a validator for checking that the regular expression matches the
+ * complete string to validate.
+ *
+ * @param regexp
+ * a Java regular expression
+ * @param errorMessage
+ * the message to display in case the value does not validate.
+ */
+ public RegexpValidator(String regexp, String errorMessage) {
+ this(regexp, true, errorMessage);
+ }
+
+ /**
+ * Creates a validator for checking that the regular expression matches the
+ * string to validate.
+ *
+ * @param regexp
+ * a Java regular expression
+ * @param complete
+ * true to use check for a complete match, false to look for a
+ * matching substring
+ * @param errorMessage
+ * the message to display in case the value does not validate.
+ */
+ public RegexpValidator(String regexp, boolean complete, String errorMessage) {
+ super(errorMessage);
+ pattern = Pattern.compile(regexp);
+ this.complete = complete;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.data.validator.AbstractValidator#isValidValue(java.lang.Object
+ * )
+ */
+ @Override
+ protected boolean isValidValue(String value) {
+ if (complete) {
+ return getMatcher(value).matches();
+ } else {
+ return getMatcher(value).find();
+ }
+ }
+
+ /**
+ * Get a new or reused matcher for the pattern
+ *
+ * @param value
+ * the string to find matches in
+ * @return Matcher for the string
+ */
+ private Matcher getMatcher(String value) {
+ if (matcher == null) {
+ matcher = pattern.matcher(value);
+ } else {
+ matcher.reset(value);
+ }
+ return matcher;
+ }
+
+}
diff --git a/server/src/com/vaadin/data/validator/StringLengthValidator.java b/server/src/com/vaadin/data/validator/StringLengthValidator.java
new file mode 100644
index 0000000000..54b2d28f58
--- /dev/null
+++ b/server/src/com/vaadin/data/validator/StringLengthValidator.java
@@ -0,0 +1,139 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.data.validator;
+
+/**
+ * This <code>StringLengthValidator</code> is used to validate the length of
+ * strings.
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+@SuppressWarnings("serial")
+public class StringLengthValidator extends AbstractStringValidator {
+
+ private Integer minLength = null;
+
+ private Integer maxLength = null;
+
+ private boolean allowNull = true;
+
+ /**
+ * Creates a new StringLengthValidator with a given error message.
+ *
+ * @param errorMessage
+ * the message to display in case the value does not validate.
+ */
+ public StringLengthValidator(String errorMessage) {
+ super(errorMessage);
+ }
+
+ /**
+ * Creates a new StringLengthValidator with a given error message and
+ * minimum and maximum length limits.
+ *
+ * @param errorMessage
+ * the message to display in case the value does not validate.
+ * @param minLength
+ * the minimum permissible length of the string or null for no
+ * limit. A negative value for no limit is also supported for
+ * backwards compatibility.
+ * @param maxLength
+ * the maximum permissible length of the string or null for no
+ * limit. A negative value for no limit is also supported for
+ * backwards compatibility.
+ * @param allowNull
+ * Are null strings permissible? This can be handled better by
+ * setting a field as required or not.
+ */
+ public StringLengthValidator(String errorMessage, Integer minLength,
+ Integer maxLength, boolean allowNull) {
+ this(errorMessage);
+ setMinLength(minLength);
+ setMaxLength(maxLength);
+ setNullAllowed(allowNull);
+ }
+
+ /**
+ * Checks if the given value is valid.
+ *
+ * @param value
+ * the value to validate.
+ * @return <code>true</code> for valid value, otherwise <code>false</code>.
+ */
+ @Override
+ protected boolean isValidValue(String value) {
+ if (value == null) {
+ return allowNull;
+ }
+ final int len = value.length();
+ if ((minLength != null && minLength > -1 && len < minLength)
+ || (maxLength != null && maxLength > -1 && len > maxLength)) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Returns <code>true</code> if null strings are allowed.
+ *
+ * @return <code>true</code> if allows null string, otherwise
+ * <code>false</code>.
+ */
+ @Deprecated
+ public final boolean isNullAllowed() {
+ return allowNull;
+ }
+
+ /**
+ * Gets the maximum permissible length of the string.
+ *
+ * @return the maximum length of the string or null if there is no limit
+ */
+ public Integer getMaxLength() {
+ return maxLength;
+ }
+
+ /**
+ * Gets the minimum permissible length of the string.
+ *
+ * @return the minimum length of the string or null if there is no limit
+ */
+ public Integer getMinLength() {
+ return minLength;
+ }
+
+ /**
+ * Sets whether null-strings are to be allowed. This can be better handled
+ * by setting a field as required or not.
+ */
+ @Deprecated
+ public void setNullAllowed(boolean allowNull) {
+ this.allowNull = allowNull;
+ }
+
+ /**
+ * Sets the maximum permissible length of the string.
+ *
+ * @param maxLength
+ * the maximum length to accept or null for no limit
+ */
+ public void setMaxLength(Integer maxLength) {
+ this.maxLength = maxLength;
+ }
+
+ /**
+ * Sets the minimum permissible length.
+ *
+ * @param minLength
+ * the minimum length to accept or null for no limit
+ */
+ public void setMinLength(Integer minLength) {
+ this.minLength = minLength;
+ }
+
+}
diff --git a/server/src/com/vaadin/data/validator/package.html b/server/src/com/vaadin/data/validator/package.html
new file mode 100644
index 0000000000..c991bfc82a
--- /dev/null
+++ b/server/src/com/vaadin/data/validator/package.html
@@ -0,0 +1,23 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
+<html>
+<head>
+
+</head>
+
+<body bgcolor="white">
+
+<!-- Package summary here -->
+
+<p>Provides various {@link com.vaadin.data.Validator}
+implementations.</p>
+
+<p>{@link com.vaadin.data.validator.AbstractValidator
+AbstractValidator} provides an abstract implementation of the {@link
+com.vaadin.data.Validator} interface and can be extended for custom
+validation needs. {@link
+com.vaadin.data.validator.AbstractStringValidator
+AbstractStringValidator} can also be extended if the value is a String.</p>
+
+
+</body>
+</html>