All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.vaadin.data.util.sqlcontainer.SQLContainer Maven / Gradle / Ivy

There is a newer version: 8.27.3
Show newest version
/*
 * Vaadin Framework 7
 *
 * Copyright (C) 2000-2024 Vaadin Ltd
 *
 * This program is available under Vaadin Commercial License and Service Terms.
 *
 * See  for the full
 * license.
 */
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.ContainerHelpers;
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 queryDelegate;
    /** 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;

    /** Amount of cache to overlap with previous page */
    private int cacheOverlap = pageLength;

    /** Item and index caches */
    private final Map itemIndexes = new HashMap();
    private final CacheMap cachedItems = new CacheMap();

    /** Container properties = column names, data types and statuses */
    private final List propertyIds = new ArrayList();
    private final Map> propertyTypes = new HashMap>();
    private final Map propertyReadOnly = new HashMap();
    private final Map propertyPersistable = new HashMap();
    private final Map propertyNullable = new HashMap();
    private final Map propertyPrimaryKey = new HashMap();

    /** Filters (WHERE) and sorters (ORDER BY) */
    private final List filters = new ArrayList();
    private final List sorters = new ArrayList();

    /**
     * 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 itemSetChangeListeners;

    /**
     * Temporary storage for modified items and items to be removed and added
     */
    private final Map removedItems = new HashMap();
    private final List addedItems = new ArrayList();
    private final List modifiedItems = new ArrayList();

    /** List of references to other SQLContainers */
    private final Map references = new HashMap();

    /** 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.");
        }
        queryDelegate = delegate;
        getPropertyIds();
        cachedItems.setCacheLimit(CACHE_RATIO * getPageLength() + cacheOverlap);
    }

    /**************************************/
    /** 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[queryDelegate.getPrimaryKeyColumns()
                .size()];
        RowId itemId = new TemporaryRowId(emptyKey);
        // Create new empty column properties for the row item.
        List itemProperties = new ArrayList();
        for (String propertyId : propertyIds) {
            /* Default settings for new item properties. */
            ColumnProperty cp = new ColumnProperty(propertyId,
                    propertyReadOnly.get(propertyId),
                    propertyPersistable.get(propertyId),
                    propertyNullable.get(propertyId),
                    propertyPrimaryKey.get(propertyId), null,
                    getType(propertyId));

            itemProperties.add(cp);
        }
        RowItem newRowItem = new RowItem(this, itemId, itemProperties);

        if (autoCommit) {
            /* Add and commit instantly */
            try {
                if (queryDelegate instanceof TableQuery) {
                    itemId = ((TableQuery) queryDelegate)
                            .storeRowImmediately(newRowItem);
                } else {
                    queryDelegate.beginTransaction();
                    queryDelegate.storeRow(newRowItem);
                    queryDelegate.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 {
                    queryDelegate.rollback();
                } catch (SQLException ee) {
                    getLogger().log(Level.SEVERE,
                            "Failed to roll back row addition", e);
                }
                return null;
            }
        } else {
            addedItems.add(newRowItem);
            fireContentsChange();
            return itemId;
        }
    }

    @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 queryDelegate
                        .containsRowWithKey(((RowId) itemId).getId());
            } catch (Exception e) {
                /* Query failed, just return false. */
                getLogger().log(Level.WARNING, "containsId query failed", e);
            }
        }
        return false;
    }

    @Override
    public Property getContainerProperty(Object itemId, Object propertyId) {
        Item item = getItem(itemId);
        if (item == null) {
            return null;
        }
        return item.getItemProperty(propertyId);
    }

    @Override
    public Collection getContainerPropertyIds() {
        return Collections.unmodifiableCollection(propertyIds);
    }

    @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.
     * NOTE: 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 ids = new ArrayList();
        ResultSet rs = null;
        try {
            // Load ALL rows :(
            queryDelegate.beginTransaction();
            rs = queryDelegate.getResults(0, 0);
            List pKeys = queryDelegate.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();
            queryDelegate.commit();
        } catch (SQLException e) {
            getLogger().log(Level.WARNING, "getItemIds() failed, rolling back.",
                    e);
            try {
                queryDelegate.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);
    }

    @Override
    public Class getType(Object propertyId) {
        if (!propertyIds.contains(propertyId)) {
            return null;
        }
        return propertyTypes.get(propertyId);
    }

    @Override
    public int size() {
        updateCount();
        return size + sizeOfAddedItems() - removedItems.size();
    }

    @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 {
                queryDelegate.beginTransaction();
                boolean success = queryDelegate.removeRow((RowItem) i);
                queryDelegate.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 {
                    queryDelegate.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 {
                    queryDelegate.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;
        }
    }

    @Override
    public boolean removeAllItems() throws UnsupportedOperationException {
        if (autoCommit) {
            /* Remove and commit instantly. */
            try {
                queryDelegate.beginTransaction();
                boolean success = true;
                for (Object id : getItemIds()) {
                    if (!queryDelegate.removeRow((RowItem) getItem(id))) {
                        success = false;
                    }
                }
                if (success) {
                    queryDelegate.commit();
                    getLogger().log(Level.FINER, "All rows removed from DB...");
                    refresh();
                    if (notificationsEnabled) {
                        CacheFlushNotifier.notifyOfCacheFlush(this);
                    }
                } else {
                    queryDelegate.rollback();
                }
                return success;
            } catch (SQLException e) {
                getLogger().log(Level.WARNING,
                        "removeAllItems() failed, rolling back", e);
                try {
                    queryDelegate.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 {
                    queryDelegate.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 toRemove = new ArrayList();
        for (Filter f : filters) {
            if (f.appliesToProperty(propertyId)) {
                toRemove.add(f);
            }
        }
        filters.removeAll(toRemove);
        refresh();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void removeAllContainerFilters() {
        filters.clear();
        refresh();
    }

    /**
     * Returns true if any filters have been applied to the container.
     *
     * @return true if the container has filters applied, false otherwise
     * @since 7.1
     */
    public boolean hasContainerFilters() {
        return !getContainerFilters().isEmpty();
    }

    @Override
    public Collection getContainerFilters() {
        return Collections.unmodifiableCollection(filters);
    }

    /**********************************************/
    /** Methods from interface Container.Indexed **/
    /**********************************************/

    @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();
        }
        // this protects against infinite looping
        int counter = 0;
        int oldIndex;
        while (counter < size) {
            if (itemIndexes.containsValue(itemId)) {
                for (Integer idx : itemIndexes.keySet()) {
                    if (itemIndexes.get(idx).equals(itemId)) {
                        return idx;
                    }
                }
            }
            oldIndex = currentOffset;
            // load in the next page.
            int nextIndex = currentOffset + pageLength * CACHE_RATIO
                    + cacheOverlap;
            if (nextIndex >= size) {
                // Container wrapped around, start from index 0.
                nextIndex = 0;
            }
            updateOffsetAndCache(nextIndex);

            // Update counter
            if (currentOffset > oldIndex) {
                counter += currentOffset - oldIndex;
            } else {
                counter += size - oldIndex;
            }
        }
        // safeguard in case item not found
        return -1;
    }

    @Override
    public Object getIdByIndex(int index) {
        if (index < 0) {
            throw new IndexOutOfBoundsException(
                    "Index is negative! index=" + index);
        }
        // make sure the size field is valid
        updateCount();
        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;
            // TODO this is very inefficient if looping - should improve
            // getItemIds(int, int)
            return getFilteredAddedItems().get(offset).getId();
        }
    }

    @Override
    public List getItemIds(int startIndex, int numberOfIds) {
        // TODO create a better implementation
        return (List) ContainerHelpers
                .getItemIdsUsingGetIdByIndex(startIndex, numberOfIds, this);
    }

    /**********************************************/
    /** Methods from interface Container.Ordered **/
    /**********************************************/

    @Override
    public Object nextItemId(Object itemId) {
        int index = indexOfId(itemId) + 1;
        try {
            return getIdByIndex(index);
        } catch (IndexOutOfBoundsException e) {
            return null;
        }
    }

    @Override
    public Object prevItemId(Object itemId) {
        int prevIndex = indexOfId(itemId) - 1;
        try {
            return getIdByIndex(prevIndex);
        } catch (IndexOutOfBoundsException e) {
            return null;
        }
    }

    @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);
    }

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

    @Override
    public boolean isFirstId(Object itemId) {
        return firstItemId().equals(itemId);
    }

    @Override
    public boolean isLastId(Object itemId) {
        return lastItemId().equals(itemId);
    }

    /***********************************************/
    /** Methods from interface Container.Sortable **/
    /***********************************************/

    @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();
    }

    @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() {
        refresh(true);
    }

    /**
     * Refreshes the container. If setSizeDirty is
     * false, assumes that the current size is up to date. This is
     * used in {@link #updateCount()} to refresh the contents when we know the
     * size was just updated.
     *
     * @param setSizeDirty
     */
    private void refresh(boolean setSizeDirty) {
        if (setSizeDirty) {
            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;
        cacheOverlap = getPageLength();
        cachedItems.setCacheLimit(CACHE_RATIO * getPageLength() + cacheOverlap);
    }

    /**
     * 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...");
            queryDelegate.beginTransaction();
            /* Perform buffered deletions */
            for (RowItem item : removedItems.values()) {
                try {
                    if (!queryDelegate.removeRow(item)) {
                        throw new SQLException(
                                "Removal failed for row with ID: "
                                        + item.getId());
                    }
                } catch (IllegalArgumentException e) {
                    throw new SQLException(
                            "Removal failed for row with ID: " + item.getId(),
                            e);
                }
            }
            /* Perform buffered modifications */
            for (RowItem item : modifiedItems) {
                if (!removedItems.containsKey(item.getId())) {
                    if (queryDelegate.storeRow(item) > 0) {
                        /*
                         * Also reset the modified state in the item in case it
                         * is reused e.g. in a form.
                         */
                        item.commit();
                    } else {
                        queryDelegate.rollback();
                        refresh();
                        throw new ConcurrentModificationException(
                                "Item with the ID '" + item.getId()
                                        + "' has been externally modified.");
                    }
                }
            }
            /* Perform buffered additions */
            for (RowItem item : addedItems) {
                queryDelegate.storeRow(item);
            }
            queryDelegate.commit();
            removedItems.clear();
            addedItems.clear();
            modifiedItems.clear();
            refresh();
            if (notificationsEnabled) {
                CacheFlushNotifier.notifyOfCacheFlush(this);
            }
        } catch (SQLException e) {
            queryDelegate.rollback();
            throw e;
        } catch (OptimisticLockException e) {
            queryDelegate.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 {
                queryDelegate.beginTransaction();
                if (queryDelegate.storeRow(changedItem) == 0) {
                    queryDelegate.rollback();
                    refresh();
                    throw new ConcurrentModificationException(
                            "Item with the ID '" + changedItem.getId()
                                    + "' has been externally modified.");
                }
                queryDelegate.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 {
                    queryDelegate.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) {

        int oldOffset = currentOffset;

        currentOffset = (index / pageLength) * pageLength - cacheOverlap;

        if (currentOffset < 0) {
            currentOffset = 0;
        }

        if (oldOffset == currentOffset && !cachedItems.isEmpty()) {
            return;
        }

        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 {
                queryDelegate.setFilters(filters);
            } catch (UnsupportedOperationException e) {
                getLogger().log(Level.FINE,
                        "The query delegate doesn't support filtering", e);
            }
            try {
                queryDelegate.setOrderBy(sorters);
            } catch (UnsupportedOperationException e) {
                getLogger().log(Level.FINE,
                        "The query delegate doesn't support sorting", e);
            }
            int newSize = queryDelegate.getCount();
            sizeUpdated = new Date();
            sizeDirty = false;
            if (newSize != size) {
                size = newSize;
                // Size is up to date so don't set it back to dirty in refresh()
                refresh(false);
            }
            getLogger().log(Level.FINER, "Updated row count. New count is: {0}",
                    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();
        queryDelegate.setFilters(null);
        queryDelegate.setOrderBy(null);
        ResultSet rs = null;
        ResultSetMetaData rsmd = null;
        try {
            queryDelegate.beginTransaction();
            rs = queryDelegate.getResults(0, 1);
            rsmd = rs.getMetaData();
            boolean resultExists = rs.next();
            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);

                boolean persistable = !rsmd.isReadOnly(i);

                if (queryDelegate instanceof TableQuery) {
                    if (rsmd.getColumnLabel(i).equals(
                            ((TableQuery) queryDelegate).getVersionColumn())) {
                        readOnly = true;
                    }
                }

                propertyReadOnly.put(colName, readOnly);
                propertyPersistable.put(colName, persistable);
                propertyNullable.put(colName,
                        rsmd.isNullable(i) == ResultSetMetaData.columnNullable);
                propertyPrimaryKey.put(colName,
                        queryDelegate.getPrimaryKeyColumns()
                                .contains(rsmd.getColumnLabel(i)));
                propertyTypes.put(colName, type);
            }
            rs.getStatement().close();
            rs.close();
            queryDelegate.commit();
            getLogger().log(Level.FINER, "Property IDs fetched.");
        } catch (SQLException e) {
            getLogger().log(Level.WARNING,
                    "Failed to fetch property ids, rolling back", e);
            try {
                queryDelegate.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 pageLength 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 {
                queryDelegate.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);
            }
            queryDelegate.beginTransaction();
            int fetchedRows = pageLength * CACHE_RATIO + cacheOverlap;
            rs = queryDelegate.getResults(currentOffset, fetchedRows);
            rsmd = rs.getMetaData();
            List pKeys = queryDelegate.getPrimaryKeyColumns();
            // }
            /* Create new items and column properties */
            ColumnProperty cp = null;
            int rowCount = currentOffset;
            if (!queryDelegate.implementationRespectsPagingLimits()) {
                rowCount = currentOffset = 0;
                setPageLengthInternal(size);
            }
            while (rs.next()) {
                List itemProperties = new ArrayList();
                /* 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 propertiesToAdd = new ArrayList(
                        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),
                                    propertyPersistable.get(colName),
                                    propertyNullable.get(colName),
                                    propertyPrimaryKey.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();
            queryDelegate.commit();
            getLogger().log(Level.FINER, "Fetched {0} rows starting from {1}",
                    new Object[] { fetchedRows, currentOffset });
        } catch (SQLException e) {
            getLogger().log(Level.WARNING, "Failed to fetch rows, rolling back",
                    e);
            try {
                queryDelegate.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 getFilteredAddedItems() {
        ArrayList filtered = new ArrayList(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")
                && queryDelegate instanceof TableQuery) {
            TableQuery tq = (TableQuery) queryDelegate;
            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 queryDelegate;
    }

    /************************************/
    /** UNSUPPORTED CONTAINER FEATURES **/
    /************************************/

    @Override
    public boolean addContainerProperty(Object propertyId, Class type,
            Object defaultValue) throws UnsupportedOperationException {
        throw new UnsupportedOperationException();
    }

    @Override
    public boolean removeContainerProperty(Object propertyId)
            throws UnsupportedOperationException {
        throw new UnsupportedOperationException();
    }

    @Override
    public Item addItem(Object itemId) throws UnsupportedOperationException {
        throw new UnsupportedOperationException();
    }

    @Override
    public Item addItemAfter(Object previousItemId, Object newItemId)
            throws UnsupportedOperationException {
        throw new UnsupportedOperationException();
    }

    @Override
    public Item addItemAt(int index, Object newItemId)
            throws UnsupportedOperationException {
        throw new UnsupportedOperationException();
    }

    @Override
    public Object addItemAt(int index) throws UnsupportedOperationException {
        throw new UnsupportedOperationException();
    }

    @Override
    public Object addItemAfter(Object previousItemId)
            throws UnsupportedOperationException {
        throw new UnsupportedOperationException();
    }

    /******************************************/
    /** ITEMSETCHANGENOTIFIER IMPLEMENTATION **/
    /******************************************/

    @Override
    public void addItemSetChangeListener(
            Container.ItemSetChangeListener listener) {
        if (itemSetChangeListeners == null) {
            itemSetChangeListeners = new LinkedList();
        }
        itemSetChangeListeners.add(listener);
    }

    /**
     * @deprecated As of 7.0, replaced by
     *             {@link #addItemSetChangeListener(Container.ItemSetChangeListener)}
     **/
    @Override
    @Deprecated
    public void addListener(Container.ItemSetChangeListener listener) {
        addItemSetChangeListener(listener);
    }

    @Override
    public void removeItemSetChangeListener(
            Container.ItemSetChangeListener listener) {
        if (itemSetChangeListeners != null) {
            itemSetChangeListeners.remove(listener);
        }
    }

    /**
     * @deprecated As of 7.0, replaced by
     *             {@link #removeItemSetChangeListener(Container.ItemSetChangeListener)}
     **/
    @Override
    @Deprecated
    public void removeListener(Container.ItemSetChangeListener listener) {
        removeItemSetChangeListener(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 addRowIdChangeListener(RowIdChangeListener listener) {
        if (queryDelegate instanceof QueryDelegate.RowIdChangeNotifier) {
            ((QueryDelegate.RowIdChangeNotifier) queryDelegate)
                    .addListener(listener);
        }
    }

    /**
     * @deprecated As of 7.0, replaced by
     *             {@link #addRowIdChangeListener(RowIdChangeListener)}
     **/
    @Deprecated
    public void addListener(RowIdChangeListener listener) {
        addRowIdChangeListener(listener);
    }

    /**
     * Removes a RowIdChangeListener from the QueryDelegate
     *
     * @param listener
     */
    public void removeRowIdChangeListener(RowIdChangeListener listener) {
        if (queryDelegate instanceof QueryDelegate.RowIdChangeNotifier) {
            ((QueryDelegate.RowIdChangeNotifier) queryDelegate)
                    .removeListener(listener);
        }
    }

    /**
     * @deprecated As of 7.0, replaced by
     *             {@link #removeRowIdChangeListener(RowIdChangeListener)}
     **/
    @Deprecated
    public void removeListener(RowIdChangeListener listener) {
        removeRowIdChangeListener(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());
    }

}