org.vaadin.tltv.multiscrolltable.ui.CustomScrollTable.java Source code

Java tutorial

Introduction

Here is the source code for org.vaadin.tltv.multiscrolltable.ui.CustomScrollTable.java

Source

/*
 * Copyright 2013 Tomi Virtanen
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.vaadin.tltv.multiscrolltable.ui;

import static org.vaadin.tltv.multiscrolltable.client.ui.TableUtil.defaultString;
import static org.vaadin.tltv.multiscrolltable.client.ui.VCustomScrollTable.ATTR_BUFFERSIZE;
import static org.vaadin.tltv.multiscrolltable.client.ui.VCustomScrollTable.ATTR_CAPTION;
import static org.vaadin.tltv.multiscrolltable.client.ui.VCustomScrollTable.ATTR_CHECK_SPACE_AVAILABLE;
import static org.vaadin.tltv.multiscrolltable.client.ui.VCustomScrollTable.ATTR_CHILDRENS_ALLOWED;
import static org.vaadin.tltv.multiscrolltable.client.ui.VCustomScrollTable.ATTR_COLS;
import static org.vaadin.tltv.multiscrolltable.client.ui.VCustomScrollTable.ATTR_COLUMN_STRUCTURE_CHANGED;
import static org.vaadin.tltv.multiscrolltable.client.ui.VCustomScrollTable.ATTR_DEPTH;
import static org.vaadin.tltv.multiscrolltable.client.ui.VCustomScrollTable.ATTR_DESCRIPTION;
import static org.vaadin.tltv.multiscrolltable.client.ui.VCustomScrollTable.ATTR_IMMEDIATE;
import static org.vaadin.tltv.multiscrolltable.client.ui.VCustomScrollTable.ATTR_INDEX;
import static org.vaadin.tltv.multiscrolltable.client.ui.VCustomScrollTable.ATTR_OPEN;
import static org.vaadin.tltv.multiscrolltable.client.ui.VCustomScrollTable.ATTR_PID;
import static org.vaadin.tltv.multiscrolltable.client.ui.VCustomScrollTable.ATTR_READONLY;
import static org.vaadin.tltv.multiscrolltable.client.ui.VCustomScrollTable.ATTR_REQFIRSTCOL;
import static org.vaadin.tltv.multiscrolltable.client.ui.VCustomScrollTable.ATTR_REQFIRSTROW;
import static org.vaadin.tltv.multiscrolltable.client.ui.VCustomScrollTable.ATTR_ROWS;
import static org.vaadin.tltv.multiscrolltable.client.ui.VCustomScrollTable.ATTR_ROWS_CHANGED;
import static org.vaadin.tltv.multiscrolltable.client.ui.VCustomScrollTable.ATTR_ROW_STRUCTURE_CHANGED;
import static org.vaadin.tltv.multiscrolltable.client.ui.VCustomScrollTable.ATTR_SCROLL_GROUPS;
import static org.vaadin.tltv.multiscrolltable.client.ui.VCustomScrollTable.ATTR_TOTALROWS;
import static org.vaadin.tltv.multiscrolltable.client.ui.VCustomScrollTable.TAG_COLUMN;
import static org.vaadin.tltv.multiscrolltable.client.ui.VCustomScrollTable.TAG_COLUMNGROUP;
import static org.vaadin.tltv.multiscrolltable.client.ui.VCustomScrollTable.TAG_COLUMNS;
import static org.vaadin.tltv.multiscrolltable.client.ui.VCustomScrollTable.TAG_ROWS;
import static org.vaadin.tltv.multiscrolltable.client.ui.VCustomScrollTable.TAG_SCROLLCONTENT;
import static org.vaadin.tltv.multiscrolltable.client.ui.VCustomScrollTable.TAG_TR;
import static org.vaadin.tltv.multiscrolltable.client.ui.VCustomScrollTable.TAG_VALUE;
import static org.vaadin.tltv.multiscrolltable.client.ui.VCustomScrollTable.VAR_NEWVALUE;
import static org.vaadin.tltv.multiscrolltable.client.ui.VCustomScrollTable.VAR_TOGGLE_COLLAPSED;

import java.io.Serializable;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Arrays;
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.List;
import java.util.Map;
import java.util.Set;

import org.vaadin.tltv.multiscrolltable.client.MultiScrollTableServerRpc;
import org.vaadin.tltv.multiscrolltable.client.MultiScrollTableState;

import com.vaadin.data.Container;
import com.vaadin.data.Container.Hierarchical;
import com.vaadin.data.Container.Indexed;
import com.vaadin.data.Container.ItemSetChangeEvent;
import com.vaadin.data.Container.ItemSetChangeListener;
import com.vaadin.data.Container.PropertySetChangeEvent;
import com.vaadin.data.Container.PropertySetChangeListener;
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.util.ContainerHierarchicalWrapper;
import com.vaadin.data.util.HierarchicalContainer;
import com.vaadin.server.KeyMapper;
import com.vaadin.server.PaintException;
import com.vaadin.server.PaintTarget;
import com.vaadin.ui.AbstractComponent;
import com.vaadin.ui.LegacyComponent;

public class CustomScrollTable extends AbstractComponent implements LegacyComponent, ValueChangeListener,
        ItemSetChangeListener, PropertySetChangeListener, ScrollContentChangeListener {

    private static final long serialVersionUID = 4875429342703360229L;

    private static final int ROW_BUFFER_SIZE = 5;

    /**
     * Keymapper for proptery ids.
     */
    protected final KeyMapper columnIdMap = new KeyMapper();

    private final Set<ScrollContent> scrollContents = new LinkedHashSet<ScrollContent>();

    protected int requestedRowsToPaint = 5;

    // requestedFirstRowToPaint is always a index from the datasource container.
    protected int requestedFirstRowToPaint = -1;

    protected int requestedFirstColToPaint = 0;

    private Collection<Object> visibleColumns = new LinkedList<Object>();

    protected Object[][] pageBuffer = null;

    protected Hierarchical datasource;

    private Object rowHeaderPropertyId;

    private Object rowDescriptionPropertyId;

    /**
     * When true, during the next paint, client will measure the available space
     * for the rows first and request new data to paint.
     */
    protected boolean measureSpaceForRowsAvailable = true;

    protected boolean columnStructureChanged = true;
    protected boolean rowStructureChanged = true;
    protected boolean rowsChanged = true;

    /*
     * This map's purpose is to keep track of the old values. Every value change
     * will change the value in the map. Key is a property id.
     */
    private final Map<Object, Object> oldValueChangeBuffer = new HashMap<Object, Object>();

    private Object valueChangePropertyId;

    private Object valueChangeItemId;

    private final MultiScrollTableServerRpc rpc = new MultiScrollTableServerRpc() {

        @Override
        public void updateFirstRowIndex(Integer newFirstRowIndex) {
            handleRowVisibilityChange(newFirstRowIndex, null);
        }

        @Override
        public void updateVisibleRowCount(Integer newVisibleRows) {
            handleRowVisibilityChange(null, newVisibleRows);
        }

        @Override
        public void updateFirstRowIndexAndVisibleRowCount(Integer newFirstRowIndex, Integer newVisibleRows) {
            handleRowVisibilityChange(newFirstRowIndex, newVisibleRows);
        }

    };

    public interface Formatter extends Serializable {
        char getGroupingSeparator();

        String format(Object number, Object propertyId);

        Number parse(Object value, Object propertyId, Object itemId, Property p) throws ParseException;
    }

    private Formatter formatter = DEFAULT_FORMATTER;

    public static final Formatter DEFAULT_FORMATTER = new Formatter() {

        private static final long serialVersionUID = 884515870065895819L;
        private final DecimalFormat decimalFormat = new DecimalFormat();

        {
            DecimalFormatSymbols symbols = new DecimalFormatSymbols();
            symbols.setGroupingSeparator(' ');
            decimalFormat.setDecimalFormatSymbols(symbols);
        }

        @Override
        public char getGroupingSeparator() {
            return decimalFormat.getDecimalFormatSymbols().getGroupingSeparator();
        }

        @Override
        public String format(Object number, Object propertyId) {
            return decimalFormat.format(number);
        }

        @Override
        public Number parse(Object value, Object propertyId, Object itemId, Property p) throws ParseException {
            if ("".equals(value) || value == null) {
                return 0;
            }
            return decimalFormat.parse(value.toString());
        }
    };

    private final ScrollContent defaultScrollContent = new ScrollContent();
    private final ColumnGroup defaultColumnGroup = new ColumnGroup();
    {
        defaultScrollContent.addColumnGroup(defaultColumnGroup);
    }

    /*
     * Gets and resets default scroll content. Scroll content will contain a
     * single caption-less ColumnGroup which contains all visible columns.
     */
    private ScrollContent getAndResetDefaultScrollContent() {
        Column c;
        defaultColumnGroup.removeAllColumns();
        for (Object o : getVisibleColumns()) {
            c = new Column(columnIdMap.key(o));
            c.setCaption(o.toString());
            defaultColumnGroup.addColumn(c);
        }
        return defaultScrollContent;
    }

    private interface ContainerStrategy extends Serializable {
        public int size();

        public boolean isNodeOpen(Object itemId);

        public int getDepth(Object itemId);

        public void toggleChildVisibility(Object itemId);

        public void expandNode(Object itemId);

        public void collapseNode(Object itemId);

        public Object getIdByIndex(int index);

        public int indexOfId(Object id);

        public Object nextItemId(Object itemId);

        public Object lastItemId();

        public Object prevItemId(Object itemId);

        public boolean isLastId(Object itemId);

        public Collection<Object> getItemIds();

        public void containerItemSetChange(ItemSetChangeEvent event);
    }

    /**
     * Strategy for Hierarchical but not Collapsible container like
     * {@link HierarchicalContainer}. Store collapsed/open states internally,
     * fool table to use preorder when accessing items from container via
     * Ordered/Indexed methods.
     */
    @SuppressWarnings("serial")
    private class HierarchicalStrategy implements ContainerStrategy {

        private final HashSet<Object> openItems = new HashSet<Object>();

        @Override
        public int getDepth(Object itemId) {
            int depth = 0;
            Hierarchical hierarchicalContainer = getContainerDataSource();
            while (!hierarchicalContainer.isRoot(itemId)) {
                depth++;
                itemId = hierarchicalContainer.getParent(itemId);
            }
            return depth;
        }

        @Override
        public boolean isNodeOpen(Object itemId) {
            return openItems.contains(itemId);
        }

        @Override
        public int size() {
            return getPreOrder().size();
        }

        @Override
        public Collection<Object> getItemIds() {
            return Collections.unmodifiableCollection(getPreOrder());
        }

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

        @Override
        public Object lastItemId() {
            if (getPreOrder().size() > 0) {
                return getPreOrder().get(getPreOrder().size() - 1);
            } else {
                return null;
            }
        }

        @Override
        public Object nextItemId(Object itemId) {
            int indexOf = getPreOrder().indexOf(itemId);
            if (indexOf == -1) {
                return null;
            }
            indexOf++;
            if (indexOf == getPreOrder().size()) {
                return null;
            } else {
                return getPreOrder().get(indexOf);
            }
        }

        @Override
        public Object prevItemId(Object itemId) {
            int indexOf = getPreOrder().indexOf(itemId);
            indexOf--;
            if (indexOf < 0) {
                return null;
            } else {
                return getPreOrder().get(indexOf);
            }
        }

        @Override
        public void toggleChildVisibility(Object itemId) {
            boolean removed = openItems.remove(itemId);
            if (!removed) {
                openItems.add(itemId);
            }
            clearPreorderCache();
        }

        @Override
        public void expandNode(Object itemId) {
            openItems.add(itemId);
            clearPreorderCache();
        }

        @Override
        public void collapseNode(Object itemId) {
            openItems.remove(itemId);
            clearPreorderCache();
        }

        private void clearPreorderCache() {
            preOrder = null; // clear preorder cache
        }

        List<Object> preOrder;

        /**
         * Preorder of ids currently visible
         * 
         * @return
         */
        private List<Object> getPreOrder() {
            if (preOrder == null) {
                preOrder = new ArrayList<Object>();
                Collection<?> rootItemIds = getContainerDataSource().rootItemIds();
                for (Object id : rootItemIds) {
                    preOrder.add(id);
                    addVisibleChildTree(id);
                }
            }
            return preOrder;
        }

        private void addVisibleChildTree(Object id) {
            if (isNodeOpen(id)) {
                Collection<?> children = getContainerDataSource().getChildren(id);
                if (children != null) {
                    for (Object childId : children) {
                        preOrder.add(childId);
                        addVisibleChildTree(childId);
                    }
                }
            }

        }

        @Override
        public int indexOfId(Object id) {
            return getPreOrder().indexOf(id);
        }

        @Override
        public Object getIdByIndex(int index) {
            return getPreOrder().get(index);
        }

        @Override
        public void containerItemSetChange(ItemSetChangeEvent event) {
            // preorder becomes invalid on sort, item additions etc.
            clearPreorderCache();
        }

    }

    public CustomScrollTable() {
        this((Container) null);
    }

    public CustomScrollTable(ScrollContent... scrollContents) {
        this(null, scrollContents);
    }

    public CustomScrollTable(Container dataSource, ScrollContent... scrollContents) {
        registerRpc(rpc, MultiScrollTableServerRpc.class);

        setContainerDataSource(dataSource);
        if (scrollContents != null && scrollContents.length > 0) {
            for (ScrollContent sc : scrollContents) {
                this.scrollContents.add(sc);
                sc.setScrollContentChangeListener(this);
            }
        } else {
            this.scrollContents.add(getAndResetDefaultScrollContent());
            defaultScrollContent.setScrollContentChangeListener(this);
        }
    }

    @Override
    public MultiScrollTableState getState() {
        return (MultiScrollTableState) super.getState();
    }

    /*
     * ContainerStrategy will handle the correct order for the hierarchical
     * content.
     */
    private ContainerStrategy containerStrategy;

    private ContainerStrategy getContainerStrategy() {
        if (containerStrategy == null) {
            containerStrategy = new HierarchicalStrategy();
        }
        return containerStrategy;
    }

    public void setContainerDataSource(Container dataSource) {
        containerStrategy = null;
        if (dataSource == null) {
            datasource = new HierarchicalContainer();
        } else if (!(dataSource instanceof Hierarchical)) {
            datasource = new ContainerHierarchicalWrapper(dataSource);
        } else {
            datasource = (Hierarchical) dataSource;
        }

        setVisibleColumns(datasource.getContainerPropertyIds());

        ((Property.ValueChangeNotifier) datasource).addListener(this);
        ((Container.ItemSetChangeNotifier) datasource).addListener(this);
        ((Container.PropertySetChangeNotifier) datasource).addListener(this);
    }

    public Hierarchical getContainerDataSource() {
        return datasource;
    }

    public Formatter getFormatter() {
        return formatter;
    }

    public void setFormatter(Formatter formatter) {
        this.formatter = formatter;
    }

    @Override
    public void paintContent(PaintTarget target) throws PaintException {
        final Object[] colids = getVisibleColumns();
        final Object[][] cells = getVisibleCells();
        final int cols = colids.length;

        target.addAttribute(ATTR_IMMEDIATE, isImmediate());
        target.addAttribute(ATTR_COLS, cols);
        target.addAttribute(ATTR_ROWS, requestedRowsToPaint);
        target.addAttribute(ATTR_TOTALROWS, size());
        target.addAttribute(ATTR_REQFIRSTROW, requestedFirstRowToPaint);
        target.addAttribute(ATTR_REQFIRSTCOL, requestedFirstColToPaint);
        target.addAttribute(ATTR_BUFFERSIZE, ROW_BUFFER_SIZE);
        target.addAttribute(ATTR_SCROLL_GROUPS, scrollContents.size());

        boolean doPaintRows = !measureSpaceForRowsAvailable;
        if (measureSpaceForRowsAvailable) {
            target.addAttribute(ATTR_CHECK_SPACE_AVAILABLE, true);
            measureSpaceForRowsAvailable = false;
        }

        paintColumns(target);
        paintFloatingRows(target);
        if (doPaintRows) {
            paintRows(target, cells, cols);
        }

    }

    /*
     * Add column data to the UIDL
     */
    private void paintColumns(PaintTarget target) throws PaintException {
        target.startTag(TAG_COLUMNS);
        if (columnStructureChanged) {
            target.addAttribute(ATTR_COLUMN_STRUCTURE_CHANGED, true);
        }
        int index = 0;

        // Paint all scroll contents
        for (ScrollContent sc : scrollContents) {
            target.startTag(TAG_SCROLLCONTENT);

            // Paint scroll content's column groups
            for (ColumnGroup cg : sc.getColumnGroups()) {
                index = paintColumnGroup(target, cg, index);
            }
            target.endTag(TAG_SCROLLCONTENT);
        }
        target.endTag(TAG_COLUMNS);
        columnStructureChanged = false;
    }

    private int paintColumnGroup(PaintTarget target, ColumnGroup cg, int index) throws PaintException {
        target.startTag(TAG_COLUMNGROUP);
        target.addAttribute(ATTR_CAPTION, defaultString(cg.getCaption()));

        if (cg instanceof HierarchicalColumnGroup) {
            // Paint column group's hierarchical structure
            for (ColumnGroup subCg : ((HierarchicalColumnGroup) cg).getSubColumnGroups()) {
                index = paintColumnGroup(target, subCg, index);
            }
        } else {
            // Paint column group's columns
            for (Column c : cg.getColumns()) {
                paintColumn(target, c, index++);
            }
        }
        target.endTag(TAG_COLUMNGROUP);

        return index;
    }

    private void paintColumn(PaintTarget target, Column c, int index) throws PaintException {
        target.startTag(TAG_COLUMN);
        target.addAttribute(ATTR_PID, columnIdMap.key(c.getColumnId()));
        target.addAttribute(ATTR_INDEX, index);
        target.addAttribute(ATTR_CAPTION, defaultString(c.getCaption()));
        target.addAttribute(ATTR_READONLY, c.isReadonly() || isReadOnly());
        // TODO

        target.endTag(TAG_COLUMN);
    }

    private void paintRows(PaintTarget target, Object[][] cells, int cols) throws PaintException {
        int index = 0;
        // Add rows and cell values to the UIDL
        if (cells != null && cols > 0) {
            target.startTag(TAG_ROWS);
            if (rowStructureChanged) {
                target.addAttribute(ATTR_ROW_STRUCTURE_CHANGED, true);
            } else if (rowsChanged) {
                target.addAttribute(ATTR_ROWS_CHANGED, true);
            }

            int size = size();
            int end = cells[0].length;
            if (end > size) {
                end = size;
            }
            String v;
            for (int i = 0; i < end; i++) {
                index = (Integer) cells[0][i];

                target.startTag(TAG_TR);
                target.addAttribute(ATTR_INDEX, index);
                Object itemId = ((Indexed) datasource).getIdByIndex(index);

                String rowHeader = getRowHeaderByIndex(index);
                if (rowHeader != null) {
                    target.addAttribute(ATTR_CAPTION, rowHeader);
                }
                String rowDescription = getRowDescriptionByIndex(index);
                if (rowDescription != null && rowDescription.length() > 0) {
                    target.addAttribute(ATTR_DESCRIPTION, rowDescription);
                }

                target.addAttribute(ATTR_DEPTH,
                        getContainerStrategy().getDepth(((Indexed) datasource).getIdByIndex(index)));

                if (getContainerDataSource().areChildrenAllowed(itemId)) {
                    target.addAttribute(ATTR_CHILDRENS_ALLOWED, true);
                    target.addAttribute(ATTR_OPEN, getContainerStrategy().isNodeOpen(itemId));
                }

                for (int j = 1; j < (cols + 1); j++) {
                    v = (String) cells[j][i];
                    if (v == null) {
                        v = "";
                    }
                    target.startTag(TAG_VALUE);
                    target.addText(v);
                    target.endTag(TAG_VALUE);
                }

                target.endTag(TAG_TR);
            }
            target.endTag(TAG_ROWS);
        }
        rowStructureChanged = false;
        rowsChanged = false;
    }

    private void paintFloatingRows(PaintTarget target) {
        // TODO Auto-generated method stub

    }

    /*
     * (non-Javadoc)
     * 
     * @see com.vaadin.ui.AbstractComponent#changeVariables(java.lang.Object,
     * java.util.Map)
     */
    @Override
    public void changeVariables(Object source, Map<String, Object> variables) {

        // TODO replcae with RPC
        handleNewValueChange(variables);
        handleHierarchyChange(variables);

    }

    /* Handle hierarchy change. */
    private void handleHierarchyChange(Map<String, Object> variables) {
        if (variables.containsKey(VAR_TOGGLE_COLLAPSED)) {
            // Handle row's hierarchy collapsing/expanding
            // Client returns a index that comes from the items container.
            Integer rowIndex = (Integer) variables.get(VAR_TOGGLE_COLLAPSED);
            Object itemId = ((Indexed) datasource).getIdByIndex(rowIndex);
            toggleChildVisibility(itemId);
        }
    }

    /* Handle cell value change. */
    private void handleNewValueChange(Map<String, Object> variables) {
        if (variables.containsKey(VAR_NEWVALUE)) {
            // Cell value has changed.
            String[] s = (String[]) variables.get(VAR_NEWVALUE);
            handleValueChange(s);
        }
    }

    /* Handle change of first visible row and/or visible row count. */
    private void handleRowVisibilityChange(Integer newFirstRowIndex, Integer newVisibleRows) {
        if (newFirstRowIndex == null && newVisibleRows == null) {
            return;
        }

        boolean doRefresh = false;
        if (newFirstRowIndex != null) {
            // requestedFirstRowToPaint is always a index from the data
            // source.
            if (newFirstRowIndex >= size()) {
                newFirstRowIndex = size() - 1;
            }
            if (newFirstRowIndex < 0) {
                newFirstRowIndex = 0;
            }
            if (getContainerStrategy().size() > newFirstRowIndex) {
                Object id = getContainerStrategy().getIdByIndex(newFirstRowIndex);
                int newReqFirstRowToPaint = ((Indexed) datasource).indexOfId(id);
                if (newReqFirstRowToPaint != requestedFirstRowToPaint) {
                    requestedFirstRowToPaint = newReqFirstRowToPaint;
                    rowsChanged = true;
                    doRefresh = true;
                }
            } else {
                requestedFirstRowToPaint = 0;
            }
        }

        if (newVisibleRows != null) {
            requestedRowsToPaint = newVisibleRows.intValue();
            rowStructureChanged = true;
            doRefresh = true;
        }
        if (doRefresh) {
            requestRefreshDataToPaint();
        }
    }

    private void toggleChildVisibility(Object itemId) {
        getContainerStrategy().toggleChildVisibility(itemId);
        rowStructureChanged = true;
        requestRefreshDataToPaint();
    }

    private void handleValueChange(String[] s) {
        if (s != null && s[0] != null && s[1] != null) {
            if (isReadOnly()) {
                markAsDirty();
                return;
            }
            // Client returns a index that comes from the items container.
            int rowIndex = Integer.parseInt(s[1]);
            if (rowIndex == -1) {
                // TODO Implement total rows
                // Header value changed
                // updateHeaderTotalValue(Integer.parseInt(s[0]), s[2]);
            } else {
                setPropertyValue(s[0], rowIndex, s[2]);
            }
        }
    }

    private void formatAndSetNewValue(Property p, Object propertyId, Object itemId, String newValue) {
        String preformattedValue = newValue;
        if (Number.class.isAssignableFrom(p.getType())) {
            try {
                Number n = formatter.parse(preformattedValue, propertyId, itemId, p);
                setPropertyValue(p, propertyId, itemId, n);
                pageBuffer = null;

                // Check the format. If it needs to be fixed, send a repaint
                // request.
                String formatted = formatter.format(n, propertyId);
                if (!preformattedValue.equals(formatted)) {
                    markAsDirty();
                }
            } catch (ParseException e) {
                // Invalid value. Request repaint to show the old value.
                markAsDirty();
            }

        } else {
            setPropertyValue(p, propertyId, itemId, preformattedValue);
        }
    }

    private void setPropertyValue(String pId, int rowIndex, String newValue) {
        Object propertyId = columnIdMap.get(pId);
        Object itemId = ((Indexed) datasource).getIdByIndex(rowIndex);
        Property p = datasource.getContainerProperty(itemId, propertyId);
        formatAndSetNewValue(p, propertyId, itemId, newValue);
    }

    /**
     * Set internal value for the target property. It is recommended to set
     * property values by this method, when it is necessary to handle table's
     * internal states like the value change would have been triggered by the
     * client.
     * 
     * @param p
     *            Target property in the container datasource
     * @param propertyId
     *            Target propery's property id
     * @param itemId
     *            Target property's item id
     * @param newVal
     *            New value object
     */
    public void setPropertyValue(Property p, Object propertyId, Object itemId, Object newVal) {
        if (p == null) {
            return;
        }

        oldValueChangeBuffer.put(propertyId, p.getValue());
        valueChangePropertyId = propertyId;
        valueChangeItemId = itemId;

        p.setValue(newVal);
    }

    /**
     * Expand a target row hierarchy.
     * 
     * @param itemId
     */
    public void expandNode(Object itemId) {
        getContainerStrategy().expandNode(itemId);
        markAsDirty();
    }

    /**
     * Collapse a target row hierarchy.
     * 
     * @param itemId
     */
    public void collapseNode(Object itemId) {
        getContainerStrategy().collapseNode(itemId);
        markAsDirty();
    }

    /**
     * Get the row header property id.
     * 
     * @return Row header property id
     */
    public Object getRowHeaderItemId() {
        return rowHeaderPropertyId;
    }

    /**
     * Set the row header property id.
     * 
     * @param rowHeaderPropertyId
     *            Row header property id
     */
    public void setRowHeaderPropertyId(Object rowHeaderPropertyId) {
        this.rowHeaderPropertyId = rowHeaderPropertyId;
        setVisibleColumns(visibleColumns.toArray());
    }

    /**
     * Get the row description id.
     * 
     * @return Row description id
     */
    public Object getRowDescriptionPropertyId() {
        return rowDescriptionPropertyId;
    }

    /**
     * Set the row description property id.
     * 
     * @param rowDescriptionPropertyId
     *            Row description property id
     */
    public void setRowDescriptionPropertyId(Object rowDescriptionPropertyId) {
        this.rowDescriptionPropertyId = rowDescriptionPropertyId;
        setVisibleColumns(visibleColumns.toArray());
    }

    /**
     * Returns property ids of visible columns.
     * 
     * @return Visible columns
     */
    public Object[] getVisibleColumns() {
        if (visibleColumns == null) {
            return null;
        }
        return visibleColumns.toArray();
    }

    private Object[][] getVisibleCells() {
        if (pageBuffer == null) {
            refreshRenderedCells();
        }
        return pageBuffer;
    }

    /**
     * Set the Collection of property identifiers that will be visible in the
     * table. Setting a value by this method will refresh the table content.
     * Unless its a null, which will throw a NullPointerException. Or
     * IllegalArgumentException when it doesn't exist in the container.
     * 
     * @param newVisibleColumns
     *            Collection of property identifiers.
     */
    public void setVisibleColumns(Collection<?> newVisibleColumns) {
        setInternalVisibleColumns(newVisibleColumns);
    }

    /**
     * Set the array of property identifiers that will be visible in the table.
     * Setting a value by this method will refresh the table content. Unless its
     * a null, which will throw a NullPointerException. Or
     * IllegalArgumentException when it doesn't exist in the container.
     * 
     * @param newVisibleColumns
     *            Array of property identifiers.
     */
    public void setVisibleColumns(Object[] newVisibleColumns) {
        // Visible columns must exist
        if (newVisibleColumns == null) {
            throw new NullPointerException("Can not set visible columns to null value");
        }
        setInternalVisibleColumns(Arrays.asList(newVisibleColumns));
    }

    protected void setInternalVisibleColumns(Collection<?> newVisibleColumns) {
        // Visible columns must exist
        if (newVisibleColumns == null) {
            throw new NullPointerException("Can not set visible columns to null value");
        }

        // Checks that the new visible columns contains no nulls and properties
        // exist
        final Collection<?> properties = datasource.getContainerPropertyIds();
        for (Object propertyCandidate : newVisibleColumns) {
            if (!properties.contains(propertyCandidate)) {
                throw new IllegalArgumentException(
                        "Property ids must exist in the Container, missing id: " + propertyCandidate);
            }
        }

        // Check for column changes
        if (visibleColumns == null || newVisibleColumns.size() != visibleColumns.size()
                || !newVisibleColumns.containsAll(visibleColumns)) {
            requestRefreshDataToPaint();
        }

        visibleColumns = Collections.unmodifiableCollection(newVisibleColumns);
    }

    /**
     * Refresh data (to be painted) instantly.
     */
    public void refreshData() {
        fixRequestedFirstColumnToPaint();
        fixRequestedFirstRowToPaint();
        refreshRenderedCells();
    }

    /**
     * Add a new ScrollContent for this table. Columns in a ScrollContent will
     * share a same vertical scroll-bar, but will have a separate horizontal
     * scroll-bars.
     * 
     * @param scrollContent
     * @return
     */
    public boolean addScrollContent(ScrollContent scrollContent) {
        // Add the new scroll content
        boolean added = scrollContents.add(scrollContent);
        if (added) {
            // Remove default scroll content
            if (scrollContents.contains(defaultScrollContent)) {
                scrollContents.remove(defaultScrollContent);
            }
            // add columns to visible columns list
            LinkedList<Object> newVisibleColumns = new LinkedList<Object>(Arrays.asList(getVisibleColumns()));
            newVisibleColumns.addAll(scrollContent.getColumnIds());
            setVisibleColumns(newVisibleColumns);

            scrollContent.setScrollContentChangeListener(this);
            scrollContentChanged();
        }
        return added;
    }

    /**
     * Returns all scroll contents that are given for this table.
     * 
     * @return Set of scroll contents
     */
    public Set<ScrollContent> getScrollContents() {
        if (!scrollContents.contains(defaultScrollContent)) {
            return Collections.unmodifiableSet(scrollContents);
        }
        return Collections.emptySet();
    }

    /**
     * Remove the target ScrollContent.
     * 
     * @param scrollContent
     * @return
     */
    public boolean removeScrollContent(ScrollContent scrollContent) {
        boolean removed = scrollContents.remove(scrollContent);
        if (removed) {
            scrollContent.setScrollContentChangeListener(null);
            if (scrollContents.size() == 0) {
                // Add a default scroll content
                scrollContents.add(getAndResetDefaultScrollContent());
            }
            scrollContentChanged();
        }
        return removed;
    }

    /**
     * Request data to be painted to be refreshed on next paint call.
     */
    public void requestRefreshDataToPaint() {
        pageBuffer = null;
        markAsDirty();
    }

    private void refreshRenderedCells() {
        final Object[] colids = getVisibleColumns();
        final int cols = colids.length;
        if (requestedFirstRowToPaint < 0) {
            resetRequestedFirstRowToPaint();
        }
        int firstIndex = requestedFirstRowToPaint;

        int totalRows = size();
        int realCols = cols + 1;
        if (totalRows == 0) {
            Object[][] cells = new Object[realCols][totalRows];
            pageBuffer = cells;
            return;
        }

        // ContainerStrategy knows the real ordered index for the item.
        int orderedIndex = getContainerStrategy().indexOfId(((Indexed) datasource).getIdByIndex(firstIndex));

        int size = totalRows;
        // As many as ROW_BUFFER_SIZE rows will be added to the start and
        // end of the content. Depending on the current position
        // (=orderedIndex).
        int reqRowsWithBuffer = requestedRowsToPaint + (2 * ROW_BUFFER_SIZE); // Initial
        // Adjust the end part of the row buffer.
        if ((orderedIndex + requestedRowsToPaint + ROW_BUFFER_SIZE) > totalRows) {
            reqRowsWithBuffer -= (orderedIndex + requestedRowsToPaint + ROW_BUFFER_SIZE) - totalRows;
        }
        // Adjust the starting part of the row buffer.
        if (orderedIndex < ROW_BUFFER_SIZE) {
            int topBuffer = ROW_BUFFER_SIZE - (ROW_BUFFER_SIZE - orderedIndex);
            orderedIndex -= topBuffer;
            reqRowsWithBuffer -= topBuffer;
        } else {
            orderedIndex -= ROW_BUFFER_SIZE;
        }

        if (reqRowsWithBuffer > 0 && reqRowsWithBuffer < size) {
            size = reqRowsWithBuffer;
        }

        // Notice buffer size
        if (orderedIndex < 0) {
            orderedIndex = 0;
        }
        firstIndex = ((Indexed) datasource).indexOfId(getContainerStrategy().getIdByIndex(orderedIndex));
        if ((orderedIndex + size) >= totalRows) {
            // Fix firstIndex when it exceeds the actual size (=totalRows)
            int indexFix = (totalRows - size);
            firstIndex = ((Indexed) datasource).indexOfId(getContainerStrategy().getIdByIndex(indexFix));
        }

        Object[][] cells = new Object[realCols][size];

        Object id = ((Indexed) datasource).getIdByIndex(firstIndex);
        int index = firstIndex;
        Object value;
        for (int i = 0; i < size && id != null; i++) {

            if (cols > 0) {
                cells[0][i] = index;
                for (int j = 0; j < cols; j++) {
                    Property p = datasource.getContainerProperty(id, colids[j]);
                    value = getPropertyValue(id, p, colids[j]);
                    cells[j + 1][i] = value;
                }
            }

            id = getContainerStrategy().nextItemId(id);
            index = ((Indexed) datasource).indexOfId(id);
        }

        pageBuffer = cells;
    }

    private String getRowHeaderByIndex(int i) {
        if (rowHeaderPropertyId == null) {
            return null;
        }
        Object id = ((Indexed) datasource).getIdByIndex(i);
        Item item = datasource.getItem(id);
        if (item == null) {
            return null;
        }
        return item.getItemProperty(rowHeaderPropertyId).toString();
    }

    private String getRowDescriptionByIndex(int i) {
        if (rowDescriptionPropertyId == null) {
            return null;
        }
        Object id = ((Indexed) datasource).getIdByIndex(i);
        Item item = datasource.getItem(id);
        if (item == null) {
            return null;
        }
        Object v = item.getItemProperty(rowDescriptionPropertyId).getValue();
        if (v != null) {
            return v.toString();
        }
        return null;
    }

    /**
     * Returns size of the data source.
     * 
     * @return Total rows
     */
    public int size() {
        return getContainerStrategy().size();
    }

    protected Object getPropertyValue(Object rowId, Property property, Object propertyId) {
        return formatPropertyValue(rowId, property, propertyId);
    }

    protected String formatPropertyValue(Object rowId, Property property, Object propertyId) {
        if (property == null || property.getValue() == null) {
            return "";
        }

        if (property.getType() != null && Number.class.isAssignableFrom(property.getType())) {
            try {
                return formatter.format(property.getValue(), propertyId);
            } catch (Exception e) {
                return "0";
            }
        }
        return property.toString();
    }

    /*
     * Fixes requestedFirstRowToPaint datasource item index. If index doesn't
     * exist (overflows), index will be set to the index of the first item in
     * the datasource.
     */
    private void fixRequestedFirstRowToPaint() {
        if (requestedFirstRowToPaint != -1) {
            if (getContainerStrategy() != null && size() > 0) {
                Object id = ((Indexed) datasource).getIdByIndex(requestedFirstRowToPaint);
                // Check that target item is still visible
                boolean existAndIsVisible = id != null && getContainerStrategy().indexOfId(id) != -1;
                if (!existAndIsVisible) {
                    // When its not, requestedFirstRowToPaint needs to updated
                    // Reset content to start from the first item
                    resetRequestedFirstRowToPaint();
                }
            } else {
                requestedFirstRowToPaint = 0;
            }
        }
    }

    /*
     * Resets the index of first requested row to be painted.
     */
    private void resetRequestedFirstRowToPaint() {
        if (getContainerStrategy() != null && size() > 0) {
            Object id = getContainerStrategy().getIdByIndex(0);
            requestedFirstRowToPaint = ((Indexed) datasource).indexOfId(id);
        } else {
            requestedFirstRowToPaint = 0;
        }
    }

    /*
     * Fixes requestedFirstColToPaint datasource property index. If index
     * doesn't exist (overflows), index will be set to the index of the first
     * property in the datasource.
     */
    private void fixRequestedFirstColumnToPaint() {
        if (requestedFirstColToPaint != -1) {
            if (requestedFirstColToPaint >= visibleColumns.size()) {
                resetRequestedFirstColumnToPaint();
            }
        }
    }

    /*
     * Resets the index of first requested column to be painted.
     */
    private void resetRequestedFirstColumnToPaint() {
        requestedFirstColToPaint = 0;
    }

    /**
     * Catch a value change and calculate a new total sum for the target column.
     */
    @Override
    public void valueChange(ValueChangeEvent event) {
        // TODO
    }

    @Override
    public void containerItemSetChange(ItemSetChangeEvent event) {
        if (containerStrategy != null) {
            containerStrategy.containerItemSetChange(event);
        }

        rowStructureChanged = true;
        requestRefreshDataToPaint();
    }

    @Override
    public void containerPropertySetChange(PropertySetChangeEvent event) {
        requestRefreshDataToPaint();
    }

    @Override
    public void scrollContentChanged() {
        columnStructureChanged = true;
        requestRefreshDataToPaint();
    }
}