org.opentaps.gwt.common.client.listviews.EntityEditableListView.java Source code

Java tutorial

Introduction

Here is the source code for org.opentaps.gwt.common.client.listviews.EntityEditableListView.java

Source

/*
 * Copyright (c) Open Source Strategies, Inc.
 *
 * Opentaps is free software: you can redistribute it and/or modify it
 * under the terms of the GNU Affero General Public License as published
 * by the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Opentaps is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with Opentaps.  If not, see <http://www.gnu.org/licenses/>.
 */

package org.opentaps.gwt.common.client.listviews;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import com.google.gwt.core.client.GWT;
import com.google.gwt.http.client.Request;
import com.google.gwt.http.client.RequestBuilder;
import com.google.gwt.http.client.RequestCallback;
import com.google.gwt.http.client.RequestException;
import com.google.gwt.http.client.Response;
import com.google.gwt.http.client.URL;
import com.gwtext.client.core.EventObject;
import com.gwtext.client.core.Function;
import com.gwtext.client.core.SortDir;
import com.gwtext.client.core.UrlParam;
import com.gwtext.client.data.BooleanFieldDef;
import com.gwtext.client.data.FieldDef;
import com.gwtext.client.data.GroupingStore;
import com.gwtext.client.data.HttpProxy;
import com.gwtext.client.data.JsonReader;
import com.gwtext.client.data.Record;
import com.gwtext.client.data.RecordDef;
import com.gwtext.client.data.Store;
import com.gwtext.client.data.StringFieldDef;
import com.gwtext.client.data.event.StoreListener;
import com.gwtext.client.widgets.Button;
import com.gwtext.client.widgets.PagingToolbar;
import com.gwtext.client.widgets.ToolTip;
import com.gwtext.client.widgets.ToolbarButton;
import com.gwtext.client.widgets.event.ButtonListenerAdapter;
import com.gwtext.client.widgets.form.ComboBox;
import com.gwtext.client.widgets.form.Field;
import com.gwtext.client.widgets.form.NumberField;
import com.gwtext.client.widgets.form.event.ComboBoxListenerAdapter;
import com.gwtext.client.widgets.form.event.FieldListenerAdapter;
import com.gwtext.client.widgets.grid.ColumnConfig;
import com.gwtext.client.widgets.grid.ColumnModel;
import com.gwtext.client.widgets.grid.EditorGridPanel;
import com.gwtext.client.widgets.grid.GridEditor;
import com.gwtext.client.widgets.grid.GridPanel;
import com.gwtext.client.widgets.grid.GroupingView;
import com.gwtext.client.widgets.grid.Renderer;
import com.gwtext.client.widgets.grid.RowParams;
import com.gwtext.client.widgets.grid.RowSelectionModel;
import com.gwtext.client.widgets.grid.event.EditorGridListenerAdapter;
import com.gwtext.client.widgets.grid.event.GridCellListenerAdapter;
import com.gwtext.client.widgets.grid.event.RowSelectionListenerAdapter;
import org.opentaps.gwt.common.client.UtilUi;
import org.opentaps.gwt.common.client.events.LoadableListener;
import org.opentaps.gwt.common.client.events.LoadableListenerAdapter;
import org.opentaps.gwt.common.client.form.FormNotificationInterface;
import org.opentaps.gwt.common.client.form.ServiceErrorReader;
import org.opentaps.gwt.common.client.form.base.BaseFormPanel;
import org.opentaps.gwt.common.client.form.field.ValuePostProcessedInterface;
import org.opentaps.gwt.common.client.lookup.Permissions;
import org.opentaps.gwt.common.client.lookup.UtilLookup;
import org.opentaps.gwt.common.client.suggest.EntityAutocomplete;
import org.opentaps.gwt.common.client.suggest.EntityStaticAutocomplete;

/**
 * The base class for tables that list entities and that support AJAX
 * sorting, pagination, filtering, and in-place edition.
 */
public abstract class EntityEditableListView extends EditorGridPanel
        implements FormNotificationInterface<Object>, StoreListener {

    private static final String MODULE = EntityEditableListView.class.getName();

    private PagingToolbar pagingToolbar;
    private ColumnModel columnModel;
    private Map<String, String> filters = new HashMap<String, String>();
    private Map<String, String> stickyFilters = new HashMap<String, String>();

    private HttpProxy proxy;
    private JsonReader reader;
    private String queryUrl;
    private GroupingStore store;
    private RecordDef recordDef;
    private RowSelectionModel selectionModel = new RowSelectionModel(true);

    private Button saveAllButton;
    private Button revertButton;

    private Set<String> recordPrimaryKeyFields;
    private Set<FieldDef> fieldDefinitions = new HashSet<FieldDef>();
    private List<ColumnConfig> columnConfigs = new ArrayList<ColumnConfig>();
    private List<LinkColumnConfig> lookupColumns = new ArrayList<LinkColumnConfig>();

    // keeps track of the autocompleters that we should wait to be loaded before loading the grid
    // see the timers below
    private List<EntityAutocomplete> autocompletes = new ArrayList<EntityAutocomplete>();

    // used to store the latest combo box changed display value
    // and so that once a cell is edited, the autocompleter display string is displayed instead of the real value
    private boolean displayStringChanged = false;
    private String displayString;

    // if the buttons are created with makeCreateUpdateColumn / makeDeleteColumn those will have the column index set
    // they are used in the cell click handler to figure out which button was clicked
    private int createUpdateIndex = -1;
    private int deleteIndex = -1;

    // default values for creating a new row
    // use setDefaultValue to override the default nulls
    // use setFirstEditableColumn to set which column editor should open after a new row was inserted
    private Object[] defaultValuesArray;
    private Map<String, Object> defaultValues = new HashMap<String, Object>();

    // use canCreateNewRow to allow record creation
    private Boolean canCreateNewRow;

    // the global permissions as parsed from the service response
    private Permissions globalPermissions;

    // set this to false to force a non editable grid
    private boolean editable = true;

    // set this to true to use a paging toolbar
    private boolean usePagingToolbar = false;

    // set to auto insert a summary row on load
    private boolean useSummaryRow = false;

    // set this to false if you need to apply filters before loading the data in order to avoid loading the data twice
    private boolean autoLoad = true;

    // the default page size when the grid and pager are initialized
    private int defaultPageSize = UtilLookup.DEFAULT_LIST_PAGE_SIZE;
    // the pagingToolbar.setPageSize method is not working properly, so we use this value as a workaround
    private int pageSize = -1;
    private NumberField pageSizeField;

    private List<LoadableListener> listeners = new ArrayList<LoadableListener>();

    // the URL to post batch data to, set when adding the save all button
    private String saveAllUrl;
    // additional data to be added to each record when batch posted
    private Map<String, String> additionalBatchData;

    // store Records for batch delete actions
    private List<Record> toDeleteRecords = new ArrayList<Record>();

    // a way to lock cells to prevent the user to edit them, normally used when those cells
    // are waiting to be filled by an Ajax event
    private Set<Cell> lockedCells = new HashSet<Cell>();

    // record autocompleter editors to columns
    private Map<String, EntityAutocomplete> columnToAutocompleter = new HashMap<String, EntityAutocomplete>();

    // record post processed editors to columns
    private Map<String, ValuePostProcessedInterface> columnToPostProcessed = new HashMap<String, ValuePostProcessedInterface>();

    private boolean loaded = false;
    private boolean loadNow = false;

    // if set the grid will use a GroupingStore and GroupingView
    private String groupField = null;
    private String groupTemplate = null;

    /** An internal class representing a Cell in the Grid. */
    public static class Cell {
        private Integer rowIndex;
        private Integer colIndex;

        /**
         * Creates a new <code>Cell</code> instance.
         * @param rowIndex the row coordinate
         * @param colIndex the column coordinate
         */
        public Cell(int rowIndex, int colIndex) {
            this.rowIndex = rowIndex;
            this.colIndex = colIndex;
        }

        /**
         * Gets the row coordinate.
         * @return an <code>int</code> value
         */
        public int getRowIndex() {
            return this.rowIndex;
        }

        /**
         * Gets the column coordinate.
         * @return an <code>int</code> value
         */
        public int getColIndex() {
            return this.colIndex;
        }

        @Override
        public int hashCode() {
            return rowIndex.hashCode() * 31 + colIndex.hashCode();
        }

        @Override
        public boolean equals(Object o) {
            if (o == null || !(o instanceof Cell)) {
                return false;
            }
            Cell c = (Cell) o;
            return (c.getRowIndex() == getRowIndex() && c.getColIndex() == getColIndex());
        }

        @Override
        public String toString() {
            return "Cell [" + rowIndex + ", " + colIndex + "]";
        }
    }

    /**
     * Default constructor.
     */
    public EntityEditableListView() {
        this(null);
    }

    /**
     * Constructor giving a title for this list view, which is displayed in the UI.
     * @param title the title of the list
     */
    public EntityEditableListView(String title) {
        if (title != null) {
            setTitle(title);
        }

        setFrame(true);
        setStripeRows(true);
        setAutoHeight(true);
        setCollapsible(true);

        setSelectionModel(selectionModel);
        setClicksToEdit(1);

        setLoadMask(true);
        // note: for some reason setLoadMask(String message) does not work, the underlying code is actually doing something wrong
        // so we use this instead and reset the default CSS class
        setLoadMask(UtilUi.MSG.loading(), "x-mask-loading");
    }

    /**
     * Configures this list view according to previously created column.
     * @param url the URL used to populate the list view
     * @param defaultSortField the name of field to sort by default
     * @see #makeColumn
     */
    protected void configure(String url, String defaultSortField) {
        configure(url, defaultSortField, SortDir.ASC);
    }

    /**
     * Configures this list view according to previously created column.
     * @param url the URL used to populate the list view
     * @param defaultSortField the name of field to sort by default
     * @param defaultSortDirection the default sort direction
     * @see #makeColumn
     */
    protected void configure(String url, String defaultSortField, SortDir defaultSortDirection) {
        configure(makeRecordDef(), makeColumnModel(), url, defaultSortField, defaultSortDirection);
    }

    protected void configure(RecordDef recordDef, ColumnModel columnModel, String url, String defaultSortField) {
        configure(recordDef, columnModel, url, defaultSortField, SortDir.ASC);
    }

    protected void configure(RecordDef recordDef, ColumnModel columnModel, String url, String defaultSortField,
            SortDir defaultSortDirection) {

        this.recordDef = recordDef;
        this.columnModel = columnModel;
        this.queryUrl = url;
        reader = new JsonReader(recordDef);
        reader.setRoot(UtilLookup.JSON_ROOT);
        reader.setId(UtilLookup.JSON_ID);
        reader.setTotalProperty(UtilLookup.JSON_TOTAL);

        proxy = new HttpProxy(queryUrl);
        store = new GroupingStore(proxy, reader, true);
        if (groupField != null) {
            store.setGroupField(groupField);
        }
        store.setDefaultSort(defaultSortField, defaultSortDirection);

        setStore(store);
        setColumnModel(columnModel);

        store.addStoreListener(this);

        if (usePagingToolbar) {
            makePagingToolbar(true);
        }

        addEditorGridListener(new EditorGridListenerAdapter() {

            // check permissions before edit
            @Override
            public boolean doBeforeEdit(GridPanel grid, Record record, String field, Object value, int rowIndex,
                    int colIndex) {
                if (isEditableCell(rowIndex, colIndex, field, value)) {
                    // If the cell is editable, then check if the cell is if associated with an Autocompleter
                    if (columnToAutocompleter.containsKey(field)) {
                        // if it is, then  bind the auto completer to this cell,  so that if it needs to set the record value
                        // later, such as after the user tabs out of it, i.e. onBlur(...),  it will know
                        EntityAutocomplete autocomplete = columnToAutocompleter.get(field);
                        autocomplete.bindToRecord(record, field, EntityEditableListView.this, rowIndex, colIndex);
                    }

                    return true;
                } else {
                    return false;
                }
            }

            @Override
            public boolean doValidateEdit(GridPanel grid, Record record, String field, Object value,
                    Object originalValue, int rowIndex, int colIndex) {
                if (columnToPostProcessed.containsKey(field)) {
                    ValuePostProcessedInterface postProcessed = columnToPostProcessed.get(field);
                    String realNewValue = postProcessed.getPostProcessedValue(originalValue, value);
                    if (realNewValue == null) {
                        return false;
                    }
                }
                return true;
            }

            // set cell edit handler, to sync displayed value when changed from a non-static autocompleter
            @Override
            public void onAfterEdit(GridPanel grid, Record record, String field, Object newValue, Object oldValue,
                    int rowIndex, int colIndex) {
                UtilUi.logDebug(
                        "Finished editing cell [" + rowIndex + "/" + colIndex + "], field: " + field + ", from "
                                + oldValue + " to " + newValue + ", displayStringChanged = " + displayStringChanged,
                        MODULE, "onAfterEdit");
                if (displayStringChanged) {
                    // copy displayString to the description field of the edited record
                    record.set(field + UtilLookup.DESCRIPTION_SUFFIX, displayString);
                    displayStringChanged = false;
                }
                // check if the value should be post processed
                if (columnToPostProcessed.containsKey(field)) {
                    ValuePostProcessedInterface postProcessed = columnToPostProcessed.get(field);
                    String realNewValue = postProcessed.getPostProcessedValue(oldValue, newValue);
                    newValue = realNewValue;
                    if (realNewValue != null) {
                        record.set(field, realNewValue);
                    }
                }
                // trigger the event handler method
                cellValueChanged(record, field, oldValue, rowIndex, colIndex);
            }
        });

        // set cell click handler for update / create and delete columns
        addGridCellListener(new GridCellListenerAdapter() {
            @Override
            public void onCellClick(GridPanel grid, int rowIndex, int colindex, EventObject e) {
                // check the grid global flag
                if (!editable) {
                    return;
                }

                Record rec = store.getRecordAt(rowIndex);
                if (rec == null) {
                    return;
                }

                if (UtilUi.isSummary(rec)) {
                    return;
                }
                if (Permissions.canUpdate(rec) && colindex == createUpdateIndex) {
                    doUpdateCreateAction(rec);
                } else if (Permissions.canDelete(rec) && colindex == deleteIndex) {
                    doDeleteAction(rec);
                }
            }
        });

        if (autoLoad) {
            UtilUi.logDebug("Auto loading data.", MODULE, "configure");
            loadFirstPage();
        }

        GroupingView view = new GroupingView() {
            @Override
            public String getRowClass(Record record, int index, RowParams rowParams, Store store) {
                return getGridViewRowClass(record, index, rowParams, store);
            }
        };
        if (groupTemplate != null) {
            view.setGroupTextTpl(groupTemplate);
        }
        view.setHideGroupedColumn(true);
        view.setEnableRowBody(true);
        view.setForceFit(true);
        view.setAutoFill(true);
        setView(view);
    }

    private String getGridViewRowClass(Record record, int index, RowParams rowParams, Store store) {
        String body = getRowBody(record, index);
        String extraClass = getRowExtraClass(record, index, body);
        String style = getRowBodyStyle(record, index, body);
        if (body != null && !"".equals(body.trim())) {
            body = "<span style=\"margin-left:15px\">" + body + "</span>";
            rowParams.setBody(body);
            if (extraClass != null) {
                extraClass = "x-grid3-row-expanded " + extraClass;
            } else {
                extraClass = "x-grid3-row-expanded";
            }
        } else {
            rowParams.setBody("");
        }
        if (style != null) {
            rowParams.setBodyStyle(style);
        }
        return extraClass;
    }

    @Override
    public void reconfigure(Store store, ColumnModel model) {
        super.reconfigure(store, model);
    }

    /**
     * Reconfigures the store and keep the current column model.
     * @param store a <code>Store</code> value
     */
    public void reconfigure(Store store) {
        reconfigure(store, columnModel);
    }

    /**
     * Set the grouping option.
     * @param groupField the field to group the results by, must be one of the Column.
     */
    public void setGrouping(String groupField) {
        this.groupField = groupField;
    }

    /**
     * Set the grouping option.
     * @param groupField the field to group the results by, must be one of the Column.
     * @param groupTemplate the template used to format the groups header
     */
    public void setGrouping(String groupField, String groupTemplate) {
        this.groupField = groupField;
        this.groupTemplate = groupTemplate;
    }

    /**
     * Sets the default page size, need to be called before {@link #configure}.
     * @param size the default page size for this list
     */
    public void setDefaultPageSize(int size) {
        this.defaultPageSize = size;
    }

    /**
     * Clears the filters of this grid.
     */
    public void clearFilters() {
        clearFilters(false);
    }

    /**
     * Clears the filters of this grid.
     * @param clearStickyFilters set to true in order to also clear the sticky filters
     */
    public void clearFilters(boolean clearStickyFilters) {
        for (String k : filters.keySet()) {
            filters.put(k, "");
        }
        if (!clearStickyFilters) {
            for (String k : stickyFilters.keySet()) {
                filters.put(k, stickyFilters.get(k));
            }
        }
    }

    protected void setFilter(String columnName, String value) {
        filters.put(columnName, value);
    }

    protected void setFilter(String columnName, String value, boolean sticky) {
        setFilter(columnName, value);
        if (sticky) {
            stickyFilters.put(columnName, value);
        }
    }

    /**
     * Applies the filters of this grid and reload at the first page.
     */
    public void applyFilters() {
        applyFilters(true);
    }

    /**
     * Applies the filters of this grid.
     * @param resetPager should the grid reloads at the first page
     */
    public void applyFilters(boolean resetPager) {
        List<UrlParam> params = new ArrayList<UrlParam>();
        for (String k : filters.keySet()) {
            params.add(new UrlParam(k, filters.get(k)));
        }
        UrlParam[] urlParams = new UrlParam[params.size()];
        store.setBaseParams(params.toArray(urlParams));
        if (resetPager) {
            UtilUi.logDebug("Applied filters, load requested.", MODULE, "applyFilters");
            loadFirstPage();
        }
    }

    /**
     * Loads the grid data.
     */
    public void loadFirstPage() {
        loadNow = true;
        // if all the registered autocompleters are already loaded, we can load now
        // else setting loadNow to true will trigger the load automatically once they are all loaded.
        if (!loadIfReady()) {
            UtilUi.logDebug("Waiting some required autocompleters to load, deferring loading data.", MODULE,
                    "loadFirstPage");
        }
    }

    /**
     * Resets the pager setting to the first page and reloads the store associated to this list view.
     */
    private void loadFirstPageAsync() {
        List<UrlParam> params = new ArrayList<UrlParam>();
        // if the pager is disabled explicitly, pass the NO_PAGER option to the service so it knows not to paginate the results
        // else pass the paging parameters as defined in the pagingToolbar (user given defaultPageSize is set in the pagingToolbar at this point)
        if (!usePagingToolbar) {
            params.add(new UrlParam(UtilLookup.PARAM_NO_PAGER, "Y"));
        } else if (pagingToolbar != null) {
            params.add(new UrlParam(UtilLookup.PARAM_PAGER_START, 0));
            if (pageSize <= 0) {
                pageSize = defaultPageSize;
            }
            params.add(new UrlParam(UtilLookup.PARAM_PAGER_LIMIT, pageSize));
        }
        UrlParam[] urlParams = new UrlParam[params.size()];
        store.reload(params.toArray(urlParams));
    }

    private boolean checkAllLoaded() {
        // check is all autocompleters are loaded
        for (EntityAutocomplete autocomplete : autocompletes) {
            if (!autocomplete.isLoaded()) {
                return false;
            }
        }
        return true;
    }

    private boolean loadIfReady() {
        // check if we should load now
        if (!loadNow) {
            return false;
        }
        if (checkAllLoaded()) {
            UtilUi.logDebug("All required autocompleters ready, loading data.", MODULE, "loadIfReady");
            loadFirstPageAsync();
            return true;
        }
        return false;
    }

    /**
     * Registers an autocompleter, the grid will wait for it to be loaded before loading its data.
     * This is useful when some of the data displayed depend on other stores to be loaded to render properly.
     * @param autocompleter an <code>EntityAutocomplete</code> value
     */
    public void registerAutocompleter(EntityAutocomplete autocompleter) {
        if (autocompleter != null) {
            autocompleter.addLoadableListener(new LoadableListenerAdapter() {
                @Override
                public void onLoad() {
                    loadIfReady();
                }
            });
            autocompletes.add(autocompleter);
        }
    }

    /**
     * Gets the last added <code>ColumnConfig</code>.
     * @return the last added <code>ColumnConfig</code>
     */
    public ColumnConfig getColumn() {
        return columnConfigs.get(columnConfigs.size() - 1);
    }

    /**
     * Gets the column index by ID.
     * @param id a <code>String</code> value
     * @return an <code>int</code> value, <code>-1</code> if the column was not found
     */
    public int getColumnIndex(String id) {
        ColumnModel m = getColumnModel();
        for (int i = 0; i < m.getColumnCount(); i++) {
            if (id.equals(m.getDataIndex(i))) {
                return i;
            }
        }
        return -1;
    }

    /**
     * Sets the hidden flag for the column with given ID.
     * @param id the column ID
     * @param hidden a <code>boolean</code> value
     */
    public void setColumnHidden(String id, boolean hidden) {
        int index = getColumnIndex(id);
        if (index >= 0) {
            getColumnModel().setHidden(index, hidden);
        }
    }

    /**
     * Creates a display column for this list view prior to configuring it.
     * This method internally creates the necessary corresponding <code>ColumnConfig</code>.
     * @param label the column title label
     * @param renderer the <code>Renderer</code> instance
     * @return the created <code>ColumnConfig</code> instance
     * @see #makeEditableColumn
     * @see #makeLinkColumn
     */
    protected ColumnConfig makeColumn(String label, Renderer renderer) {
        ColumnConfig col = new ColumnConfig();
        col.setHeader(label);
        col.setRenderer(renderer);
        columnConfigs.add(col);
        return col;
    }

    /**
     * Creates a data column for this list view prior to configuring it.
     * This method internally creates the necessary corresponding <code>ColumnConfig</code>.
     * @param label the column title label
     * @param definition a <code>FieldDef</code> value
     * @return the created <code>ColumnConfig</code> instance
     * @see #makeEditableColumn
     * @see #makeLinkColumn
     */
    protected ColumnConfig makeColumn(String label, FieldDef definition) {
        return makeEditableColumn(label, definition, (GridEditor) null);
    }

    /**
     * Creates a data column for this list view prior to configuring it.
     * This method internally creates the necessary corresponding <code>ColumnConfig</code>.
     * @param label the column title label
     * @param definition a <code>FieldDef</code> value
     * @param field a <code>Field</code> instance from which to create the <code>GridEditor</code>
     * @return the created <code>ColumnConfig</code> instance
     * @see #makeColumn
     * @see #makeLinkColumn
     */
    protected ColumnConfig makeEditableColumn(String label, FieldDef definition, Field field) {
        if (field instanceof ValuePostProcessedInterface) {
            columnToPostProcessed.put(definition.getName(), (ValuePostProcessedInterface) field);
        }

        return makeEditableColumn(label, definition, new GridEditor(field));
    }

    /**
     * Creates a data column for this list view prior to configuring it.
     * This method internally creates the necessary corresponding <code>ColumnConfig</code>.
     * @param label the column title label
     * @param definition a <code>FieldDef</code> value
     * @param staticAutocomplete an <code>EntityStaticAutocomplete</code> instance from which to create the <code>GridEditor</code>, also serves as the translator
     * @return the created <code>ColumnConfig</code> instance
     * @see #makeColumn
     * @see #makeLinkColumn
     */
    protected ColumnConfig makeEditableColumn(String label, FieldDef definition,
            EntityStaticAutocomplete staticAutocomplete) {
        staticAutocomplete.setEmptyText("");
        return makeEditableColumn(label, definition, new GridEditor(staticAutocomplete), staticAutocomplete, true);
    }

    /**
     * Creates a data column for this list view prior to configuring it.
     * This method internally creates the necessary corresponding <code>ColumnConfig</code>.
     * @param label the column title label
     * @param definition a <code>FieldDef</code> value
     * @param autocomplete an <code>EntityAutocomplete</code> instance from which to create the <code>GridEditor</code>, will also sync the display value to the description field
     * @return the created <code>ColumnConfig</code> instance
     * @see #makeColumn
     * @see #makeLinkColumn
     */
    protected ColumnConfig makeEditableColumn(String label, FieldDef definition, EntityAutocomplete autocomplete) {
        return makeEditableColumn(label, definition, autocomplete, (String) null);
    }

    /**
     * Creates a data column for this list view prior to configuring it.
     * This method internally creates the necessary corresponding <code>ColumnConfig</code>.
     * @param label the column title label
     * @param definition a <code>FieldDef</code> value
     * @param autocomplete an <code>EntityAutocomplete</code> instance from which to create the <code>GridEditor</code>, will also sync the display value to the description field
     * @param initialFormatter the String used to format the displayed string, {0} is the description from the record descriptionIndex, {1} is the id from the record dataIndex
     * @return the created <code>ColumnConfig</code> instance
     * @see #makeColumn
     * @see #makeLinkColumn
     */
    protected ColumnConfig makeEditableColumn(String label, FieldDef definition, EntityAutocomplete autocomplete,
            String initialFormatter) {
        autocomplete.setEmptyText("");
        ColumnConfig col = makeEditableColumn(label, definition, new GridEditor(autocomplete), null, true,
                initialFormatter);
        columnToAutocompleter.put(definition.getName(), autocomplete);
        autocomplete.addListener(new ComboBoxListenerAdapter() {
            @Override
            public void onSelect(ComboBox comboBox, Record record, int index) {
                // get the display value, we don't know which cell was edited, so save the value for later
                displayString = record.getAsString(UtilLookup.SUGGEST_TEXT);
                // marked it changed so the grid cell edit event will know to get it
                displayStringChanged = true;
                UtilUi.logDebug("An autocompleter changed, got displayString = " + displayString, MODULE,
                        "onSelect");
            }
        });
        return col;
    }

    /**
     * Creates a data column for this list view prior to configuring it.
     * This method internally creates the necessary corresponding <code>ColumnConfig</code>.
     * @param label the column title label
     * @param definition a <code>FieldDef</code> value
     * @param autocomplete an <code>EntityAutocomplete</code> instance from which to create the <code>GridEditor</code>
     * @param staticAutocomplete an <code>EntityAutocomplete</code> instance from which to create the translator, it will have to be made static meaning that all records will be fetched so this is not recommended
     * @return the created <code>ColumnConfig</code> instance
     * @see #makeColumn
     * @see #makeLinkColumn
     */
    protected ColumnConfig makeEditableColumn(String label, FieldDef definition, EntityAutocomplete autocomplete,
            EntityAutocomplete staticAutocomplete) {
        autocomplete.setEmptyText("");
        staticAutocomplete.makeStatic();
        return makeEditableColumn(label, definition, new GridEditor(autocomplete), staticAutocomplete, true);
    }

    /**
     * Creates a data column for this list view prior to configuring it.
     * This method internally creates the necessary corresponding <code>ColumnConfig</code>.
     * @param label the column title label
     * @param definition a <code>FieldDef</code> value
     * @param editor a <code>GridEditor</code> instance
     * @return the created <code>ColumnConfig</code> instance
     * @see #makeColumn
     * @see #makeLinkColumn
     */
    protected ColumnConfig makeEditableColumn(String label, FieldDef definition, GridEditor editor) {
        return makeEditableColumn(label, definition, editor, null, false);
    }

    /**
     * Creates a data column for this list view prior to configuring it.
     * This method internally creates the necessary corresponding <code>ColumnConfig</code>.
     * @param label the column title label
     * @param definition a <code>FieldDef</code> value
     * @param editor a <code>GridEditor</code> instance
     * @param autocomplete the <code>EntityAutocomplete</code> instance serving as the translator
     * @param useDescriptionColumn a flag to indicate if we should use a description column config
     * @return the created <code>ColumnConfig</code> instance
     * @see #makeColumn
     * @see #makeLinkColumn
     */
    private ColumnConfig makeEditableColumn(String label, FieldDef definition, GridEditor editor,
            EntityAutocomplete autocomplete, boolean useDescriptionColumn) {
        return makeEditableColumn(label, definition, editor, autocomplete, useDescriptionColumn, null);
    }

    /**
     * Creates a data column for this list view prior to configuring it.
     * This method internally creates the necessary corresponding <code>ColumnConfig</code>.
     * @param label the column title label
     * @param definition a <code>FieldDef</code> value
     * @param editor a <code>GridEditor</code> instance
     * @param autocomplete the <code>EntityAutocomplete</code> instance serving as the translator
     * @param useDescriptionColumn a flag to indicate if we should use a description column config
     * @param initialFormatter the String used to format the initial displayed string, {0} is the description from the record descriptionIndex, {1} is the id from the record dataIndex
     * @return the created <code>ColumnConfig</code> instance
     * @see #makeColumn
     * @see #makeLinkColumn
     */
    private ColumnConfig makeEditableColumn(String label, FieldDef definition, GridEditor editor,
            EntityAutocomplete autocomplete, boolean useDescriptionColumn, String initialFormatter) {
        fieldDefinitions.add(definition);

        ColumnConfig col;
        if (useDescriptionColumn) {
            if (autocomplete != null) {
                registerAutocompleter(autocomplete);
                col = new DescriptionColumnConfig(label, definition.getName(), autocomplete);
            } else {
                String descriptionField = definition.getName() + UtilLookup.DESCRIPTION_SUFFIX;
                fieldDefinitions.add(new StringFieldDef(descriptionField));
                if (initialFormatter != null) {
                    col = new DescriptionColumnConfig(label, definition.getName(), descriptionField,
                            initialFormatter);
                } else {
                    col = new DescriptionColumnConfig(label, definition.getName(), descriptionField);
                }
            }
        } else {
            col = new ColumnConfig(label, definition.getName());
        }

        col.setId(definition.getName());

        if (editor != null) {
            col.setEditor(editor);
        }
        columnConfigs.add(col);
        return col;
    }

    protected void addFieldDefinition(FieldDef definition) {
        fieldDefinitions.add(definition);
    }

    /**
     * Creates a data column for this list view prior to configuring it which renders as a link to the given URL.
     * This method internally creates the necessary corresponding <code>ColumnConfig</code>.
     * @param label the column title label
     * @param valueDefinition a <code>FieldDef</code> for the field containing the amount
     * @param currencyCode the currency code string (a 3 chars code)
     * @return the created <code>ColumnConfig</code> instance
     * @see #makeColumn
     */
    protected ColumnConfig makeCurrencyColumn(String label, FieldDef valueDefinition, String currencyCode) {
        if (fieldDefinitions == null) {
            fieldDefinitions = new HashSet<FieldDef>();
        }
        if (columnConfigs == null) {
            columnConfigs = new ArrayList<ColumnConfig>();
        }

        fieldDefinitions.add(valueDefinition);

        CurrencyColumnConfig col = new CurrencyColumnConfig(label, valueDefinition.getName());
        col.setCurrencyCode(currencyCode);
        col.setId(valueDefinition.getName());
        columnConfigs.add(col);
        return col;
    }

    /**
     * Creates a data column for this list view prior to configuring it.
     * This method internally creates the necessary corresponding <code>ColumnConfig</code>.
     * @param label the column title label
     * @param currencyDefinition a <code>FieldDef</code> for the field containing the currency code
     * @param valueDefinition a <code>FieldDef</code> for the field containing the amount
     * @return the created <code>ColumnConfig</code> instance
     * @see #makeColumn
     */
    protected ColumnConfig makeCurrencyColumn(String label, FieldDef currencyDefinition, FieldDef valueDefinition) {
        if (fieldDefinitions == null) {
            fieldDefinitions = new HashSet<FieldDef>();
        }
        if (columnConfigs == null) {
            columnConfigs = new ArrayList<ColumnConfig>();
        }

        fieldDefinitions.add(valueDefinition);

        // the currency field definition might not have been added yet, this is safe since fieldDefinitions is a Set
        fieldDefinitions.add(currencyDefinition);

        CurrencyColumnConfig col = new CurrencyColumnConfig(label, currencyDefinition.getName(),
                valueDefinition.getName());
        col.setId(valueDefinition.getName());
        columnConfigs.add(col);
        return col;
    }

    /**
     * Creates a data column for this list view prior to configuring it which renders as a link to the given URL.
     * This method internally creates the necessary corresponding <code>ColumnConfig</code>.
     * @param label the column title label
     * @param valueDefinition a <code>FieldDef</code> value
     * @param url the URL to be used for making the link, a placeholder can be used in the string for the field data. For example <code>/crmsfa/control/viewContact?partyId={0}</code>
     * @return the created <code>ColumnConfig</code> instance
     * @see #makeColumn
     */
    protected ColumnConfig makeLinkColumn(String label, FieldDef valueDefinition, String url) {
        return makeLinkColumn(label, valueDefinition, url, false);
    }

    /**
     * Creates a data column for this list view prior to configuring it which renders as a link to the given URL.
     * This method internally creates the necessary corresponding <code>ColumnConfig</code>.
     * @param label the column title label
     * @param valueDefinition a <code>FieldDef</code> value
     * @param url the URL to be used for making the link, a placeholder can be used in the string for the field data. For example <code>/crmsfa/control/viewContact?partyId={0}</code>
     * @param lookup if <code>true</code> the link will be replaced by a javascript call that set the value to return when the widget is used as a lookup
     * @return the created <code>ColumnConfig</code> instance
     * @see #makeColumn
     */
    protected ColumnConfig makeLinkColumn(String label, FieldDef valueDefinition, String url, boolean lookup) {
        return makeLinkColumn(label, valueDefinition, valueDefinition, url, lookup);
    }

    /**
     * Creates a data column for this list view prior to configuring it which renders as a link to the given URL.
     * This method internally creates the necessary corresponding <code>ColumnConfig</code>.
     * @param label the column title label
     * @param idDefinition a <code>FieldDef</code> value
     * @param valueDefinition a <code>FieldDef</code> value
     * @param url the URL to be used for making the link, a placeholder can be used in the string for the ID data. For example <code>/crmsfa/control/viewContact?partyId={0}</code>
     * @return the created <code>ColumnConfig</code> instance
     * @see #makeColumn
     */
    protected ColumnConfig makeLinkColumn(String label, FieldDef idDefinition, FieldDef valueDefinition,
            String url) {
        return makeLinkColumn(label, idDefinition, valueDefinition, url, false);
    }

    /**
     * Creates a data column for this list view prior to configuring it.
     * This method internally creates the necessary corresponding <code>ColumnConfig</code>.
     * @param label the column title label
     * @param idDefinition a <code>FieldDef</code> value
     * @param valueDefinition a <code>FieldDef</code> value
     * @param url the URL to be used for making the link, a placeholder can be used in the string for the ID data. For example <code>/crmsfa/control/viewContact?partyId={0}</code>
     * @param lookup if <code>true</code> the link will be replaced by a javascript call that set the value to return when the widget is used as a lookup
     * @return the created <code>ColumnConfig</code> instance
     * @see #makeColumn
     */
    protected ColumnConfig makeLinkColumn(String label, FieldDef idDefinition, FieldDef valueDefinition, String url,
            boolean lookup) {
        if (fieldDefinitions == null) {
            fieldDefinitions = new HashSet<FieldDef>();
        }
        if (columnConfigs == null) {
            columnConfigs = new ArrayList<ColumnConfig>();
        }

        fieldDefinitions.add(valueDefinition);

        // the ID field definition might not have been added yet, this is safe since fieldDefinitions is a Set
        fieldDefinitions.add(idDefinition);

        LinkColumnConfig col = new LinkColumnConfig(label, idDefinition.getName(), valueDefinition.getName(), url,
                lookup);
        col.setId(valueDefinition.getName());
        if (lookup) {
            lookupColumns.add(col);
        }

        columnConfigs.add(col);
        return col;
    }

    /**
     * Adds a reload button to the grid for reverting any changes made.
     */
    protected void makeReloadButton() {
        revertButton = new Button(UtilUi.MSG.revert(), new ButtonListenerAdapter() {
            @Override
            public void onClick(Button button, EventObject e) {
                loadFirstPage();
            }
        });
        addButton(revertButton);
    }

    /**
     * Adds a save all button to the grid for batch commit.
     * @param url the URL to post the batch data to
     */
    protected void makeSaveAllButton(String url) {
        makeSaveAllButton(url, null);
    }

    /**
     * Adds a save all button to the grid for batch commit.
     * @param url the URL to post the batch data to
     * @param additionalBatchData extra data that should be attache to each record when batch posting
     */
    protected void makeSaveAllButton(String url, Map<String, String> additionalBatchData) {
        this.additionalBatchData = additionalBatchData;
        saveAllButton = new Button(UtilUi.MSG.saveAll(), new ButtonListenerAdapter() {
            @Override
            public void onClick(Button button, EventObject e) {
                doBatchAction();
            }
        });
        addButton(saveAllButton);
        saveAllUrl = url;
    }

    /**
     * Creates the update button column, which can do update / create buttons.
     * @param idFieldName the field used as ID for the record, the button will be a create button if the id is <code>null</code>, else it will be an update button
     * @see #makeColumn
     */
    protected void makeCreateUpdateColumn(String idFieldName) {
        createUpdateIndex = columnConfigs.size();
        columnConfigs.add(new CreateUpdateColumnConfig(idFieldName));
    }

    /**
     * Creates the delete button column.
     * @param idFieldName the field used as ID for the record, the button will simply delete the row if the id is <code>null</code>, else it will have to post a delete request
     * @see #makeColumn
     */
    protected void makeDeleteColumn(String idFieldName) {
        deleteIndex = columnConfigs.size();
        columnConfigs.add(new DeleteColumnConfig(idFieldName));
    }

    /**
     * Creates the create / update and delete button columns.
     * @param idFieldName the field used as ID for the record
     * @see #makeCreateUpdateColumn
     * @see #makeDeleteColumn
     */
    protected void makeCUDColumns(String idFieldName) {
        makeCreateUpdateColumn(idFieldName);
        makeDeleteColumn(idFieldName);
    }

    /**
     * Sets the flag to allow creation of new records using this grid, must also have the CREATE permission returned by the service not <code>False</code>. Defaults to <code>false</code>.
     * @param flag a <code>boolean</code> value
     */
    protected void setCanCreateNewRow(boolean flag) {
        canCreateNewRow = flag;
    }

    /**
     * Sets the editable mode for this grid. Defaults to <code>true</code>.
     * To act like a simple list view set this to <code>false</code>.
     * Note: this must be set before the data is loaded.
     * @param flag a <code>boolean</code> value
     */
    public void setEditable(boolean flag) {
        editable = flag;
    }

    /**
     * Sets the data auto loading flag for this grid, if <code>true</code> the data is loaded as soon as the columns are configured, if you need to apply filters set this to <code>false</code>. Defaults to <code>true</code>.
     * Note: obviously this must be set before the data is loaded.
     * @param flag a <code>boolean</code> value
     */
    public void setAutoLoad(boolean flag) {
        autoLoad = flag;
    }

    /**
     * Sets the grid to use a summary row. Defaults to <code>false</code>.
     * Note: this must be set before the grid data is loaded.
     * @param flag a <code>boolean</code> value
     */
    public void setUseSummaryRow(boolean flag) {
        useSummaryRow = flag;
    }

    /**
     * Sets the grid to use a paging toolbar. Defaults to <code>false</code>.
     * Note: this must be set before the grid columns are configured.
     * @param flag a <code>boolean</code> value
     */
    public void setUsePagingToolbar(boolean flag) {
        usePagingToolbar = flag;
    }

    /**
     * Creates the pagination toolbar with the excel export button.
     * @param exportToExcel option to create the excel export button or not
     */
    private void makePagingToolbar(boolean exportToExcel) {

        pagingToolbar = new PagingToolbar(store);
        pagingToolbar.setPageSize(defaultPageSize);
        pagingToolbar.setDisplayInfo(true);
        pagingToolbar.setDisplayMsg(UtilUi.MSG.pagerDisplayMessage());
        pagingToolbar.setEmptyMsg(UtilUi.MSG.pagerDisplayEmpty());
        pagingToolbar.setFirstText(UtilUi.MSG.pagerFirstPage());
        pagingToolbar.setLastText(UtilUi.MSG.pagerLastPage());
        pagingToolbar.setNextText(UtilUi.MSG.pagerNextPage());
        pagingToolbar.setPrevText(UtilUi.MSG.pagerPreviousPage());
        pagingToolbar.setRefreshText(UtilUi.MSG.refresh());
        pagingToolbar.setBeforePageText(UtilUi.MSG.pagerBeforePage());
        pagingToolbar.setAfterPageText(UtilUi.MSG.pagerAfterPage());

        pageSizeField = new NumberField();
        pageSizeField.setAllowDecimals(false);
        pageSizeField.setWidth(40);
        pageSizeField.setValue(Integer.valueOf(pagingToolbar.getPageSize()));
        pageSizeField.setSelectOnFocus(true);
        pageSizeField.addListener(new FieldListenerAdapter() {
            @Override
            public void onSpecialKey(Field field, EventObject e) {
                if (e.getKey() == EventObject.ENTER) {
                    changePageSize(pageSizeField);
                }
            }
        });

        pagingToolbar.doOnRender(new Function() {
            public void execute() {
                pagingToolbar.getRefreshButton().addListener(new ButtonListenerAdapter() {
                    public void onClick(Button button, EventObject e) {
                        changePageSize(pageSizeField);
                    }
                });
            }
        });

        final ToolTip toolTip = new ToolTip(UtilUi.MSG.pagerEnterPageSize());
        toolTip.applyTo(pageSizeField);
        pagingToolbar.addField(pageSizeField);
        pagingToolbar.addText(UtilUi.MSG.pagerPageSize());

        if (exportToExcel) {
            pagingToolbar.addSeparator();
            final ToolbarButton exportToExcelButton = new ToolbarButton(UtilUi.MSG.pagerExportToExcel(),
                    new ButtonListenerAdapter() {
                        @Override
                        public void onClick(Button button, EventObject e) {
                            String url = queryUrl + "?" + UtilLookup.PARAM_EXPORT_EXCEL + "=Y";
                            // pass the filter parameters, the excel spreadsheet content will match the list view content
                            for (String k : filters.keySet()) {
                                url += "&" + k + "=" + filters.get(k);
                            }
                            // pass the sorting info
                            url += "&" + UtilLookup.PARAM_SORT_FIELD + "=" + getStore().getSortState().getField();
                            url += "&" + UtilLookup.PARAM_SORT_DIRECTION + "="
                                    + getStore().getSortState().getDirection().getDirection();
                            // pass the column info, since the user can hide and reorder columns, the excel spreadsheet will match the list view configuration
                            ColumnModel m = getColumnModel();
                            for (int i = 0; i < m.getColumnCount(); i++) {
                                // call to getDataIndex may rise error for column w/o underlying
                                // data field, e.g. column that renders a button.
                                try {
                                    url += "&_" + m.getDataIndex(i) + "_idx=" + i;
                                } catch (Exception ex) {
                                    UtilUi.logWarning(
                                            "Column with index " + Integer.valueOf(i).toString()
                                                    + " was skipped due to an exception.",
                                            MODULE, "exportToExcelButton.onClick");
                                    continue;
                                }
                            }
                            UtilUi.logInfo("url : " + url, MODULE, "exportToExcelButton.onClick");
                            UtilUi.redirect(url);
                        }
                    }, UtilUi.ICON_EXCEL);
            pagingToolbar.addButton(exportToExcelButton);
        }
        setBottomToolbar(pagingToolbar);
    }

    /**
     * Sets the page size for this list.
     * @param pageSize an integer value
     */
    public void setPageSize(int pageSize) {
        // do not allow 0 as a page size
        if (pageSize > 0) {
            this.pageSize = pageSize;
            pagingToolbar.setPageSize(pageSize);
            pageSizeField.setValue(pageSize);
        } else {
            UtilUi.logError("NOT setting negative list page size " + pageSize, MODULE, "setPageSize");
        }
    }

    /**
     * Change page size value to pageSizeField.getValue().
     * @param pageSizeField a <code>NumberField</code> value
     */
    private void changePageSize(NumberField pageSizeField) {
        // Seems using getValue().intValue() sometimes does not work
        String pageSizeString = pageSizeField.getRawValue();
        int pageSize;
        if (UtilUi.isEmpty(pageSizeString)) {
            pageSize = defaultPageSize;
        } else {
            pageSize = Integer.valueOf(pageSizeString);
        }
        // do not allow 0 as a page size
        if (pageSize > 0) {
            pageSizeField.setValue(pageSize);
            this.pageSize = pageSize;
            pagingToolbar.setPageSize(pageSize);
        } else {
            pageSizeField.setValue(Integer.valueOf(pagingToolbar.getPageSize()));
        }
    }

    /**
     * Checks if the store has been loaded (loading is asynchronous).
     * @return a <code>boolean</code> value
     */
    public boolean isLoaded() {
        return loaded;
    }

    protected ColumnModel makeColumnModel() {
        ColumnModel model = new ColumnModel(columnConfigs.toArray(new ColumnConfig[columnConfigs.size()]));
        // allow sort by default on ready only grids (as it wont conflict with cell editors)
        model.setDefaultSortable(!editable);
        return model;
    }

    protected RecordDef makeRecordDef() {
        // add the definition needed to support summary records
        addFieldDefinition(new StringFieldDef(UtilUi.SUMMARY_ROW_INDICATOR_FIELD));
        // add permissions related record definitions
        addFieldDefinition(new BooleanFieldDef(Permissions.CREATE_FIELD_NAME));
        addFieldDefinition(new BooleanFieldDef(Permissions.UPDATE_FIELD_NAME));
        addFieldDefinition(new BooleanFieldDef(Permissions.DELETE_FIELD_NAME));
        return new RecordDef(fieldDefinitions.toArray(new FieldDef[fieldDefinitions.size()]));
    }

    /** {@inheritDoc} */
    public void notifySuccess(Object obj) {
        loadFirstPage();
    }

    /**
     * Sets the list view for a lookup.
     */
    public void setLookupMode() {
        for (LinkColumnConfig lookupColumn : lookupColumns) {
            lookupColumn.setLookupMode();
        }
    }

    /**
     * Binds the list view to the given <code>BaseFormPanel</code> so that when a record is selected in the list the form content gets populated by the corresponding data,
     *  and inversely when a field is updated in the form.
     * Note that the form field names and the data field names must match.
     * @param formPanel a <code>BaseFormPanel</code> value
     */
    public void bindToForm(final BaseFormPanel formPanel) {
        if (formPanel == null) {
            return;
        }

        formPanel.setBindedList(this);

        selectionModel.addListener(new RowSelectionListenerAdapter() {
            @Override
            public void onRowSelect(RowSelectionModel sm, int rowIndex, Record record) {
                formPanel.loadRecord(record, rowIndex);
            }
        });
    }

    /**
     * Update the record at the given index with the values of the given record.
     * @param index the index in this grid store
     * @param record a <code>Record</code> value with the new values
     */
    public void updateRecord(int index, Record record) {
        if (index < 0 || index > getStore().getCount()) {
            return;
        }
        Record rec = getStore().getAt(index);
        if (rec == null) {
            return;
        }
        // synchronize the fields values, not only get the fields from record which
        // may have less fields than rec
        for (String f : record.getModifiedFields()) {
            rec.set(f, record.getAsObject(f));
        }

    }

    private void regenDefaultValuesArray() {
        if (recordDef != null) {
            List<Object> values = new ArrayList<Object>();
            for (FieldDef fd : recordDef.getFields()) {
                String fn = fd.getName();
                if (defaultValues.containsKey(fn)) {
                    values.add(defaultValues.get(fn));
                } else {
                    // handle default permissions
                    if (Permissions.CREATE_FIELD_NAME.equals(fn)) {
                        values.add(true);
                    } else if (Permissions.UPDATE_FIELD_NAME.equals(fn)) {
                        values.add(true);
                    } else if (Permissions.DELETE_FIELD_NAME.equals(fn)) {
                        values.add(false);
                    } else {
                        values.add(null);
                    }
                }

            }
            defaultValuesArray = values.toArray();
        }
    }

    /**
     * Sets the default value for the given field, used when creating a new row.
     * Defaults to <code>null</code>.
     * @param field the field name, corresponding to its <code>RecordDef</code>
     * @param value an <code>Object</code> value
     */
    protected void setDefaultValue(String field, Object value) {
        defaultValues.put(field, value);
        if (defaultValuesArray != null) {
            regenDefaultValuesArray();
        }
    }

    /**
     * Gets the first summary found in the Store.
     * @return a <code>Record</code> value
     */
    protected Record getSummaryRecord() {
        for (Record rec : store.getRecords()) {
            if (UtilUi.isSummary(rec)) {
                return rec;
            }
        }
        return null;
    }

    /**
     * Inserts a summary row at the end of the list, this can be used by subclasses to display summary information for each columns.
     * @return the created <code>Record</code> object or null if no record could be created
     */
    protected Record addSummaryRow() {
        UtilUi.logDebug("Adding summary row.", MODULE, "addSummaryRow");
        HashMap<String, Object> values = new HashMap<String, Object>();
        values.put(UtilUi.SUMMARY_ROW_INDICATOR_FIELD, "Y");
        return addRow(values);
    }

    /**
     * Inserts a new row at the end of the list with the default values IF the create permission flag is set or the grid has the <code>canCreateNewRow</code> flag to true.
     * @return the created <code>Record</code> object or null if no record could be created
     */
    protected Record addRowIfCreatePermission() {
        // check the grid editable flag first, then the canCreateNewRow, then permissions
        if (editable && canCreateNewRow) {
            if (globalPermissions.canCreate()) {
                return addRow();
            }
        }
        return null;
    }

    /**
     * Inserts a new row at the end of the list with the default values IF the create permission flag is set or the grid has the <code>canCreateNewRow</code> flag to true.
     * @param index the index where to insert the row, use negative for an index relative to the end of the list
     * @return the created <code>Record</code> object or null if no record could be created
     */
    protected Record addRowIfCreatePermission(int index) {
        // check the grid editable flag first, then the canCreateNewRow, then permissions
        if (editable && canCreateNewRow) {
            if (globalPermissions.canCreate()) {
                return addRow(index);
            }
        }
        return null;
    }

    /**
     * Inserts a new row at the end of the list with the default values.
     * @return the created <code>Record</code> object or null if no record could be created
     */
    protected Record addRow() {
        return addRow(store.getCount());
    }

    /**
     * Inserts a new row at the given index of the list with the default values.
     * @param index the index where to insert the row, use negative for an index relative to the end of the list
     * @return the created <code>Record</code> object or null if no record could be created
     */
    protected Record addRow(int index) {
        if (defaultValuesArray == null) {
            regenDefaultValuesArray();
        }
        // allow negative index as relative to the end of the list
        if (index < 0) {
            index = store.getCount() + index;
        }
        Record newRecord = recordDef.createRecord(defaultValuesArray);
        store.insert(index, newRecord);
        return newRecord;
    }

    /**
     * Inserts a new row at the given index of the list with given values.
     * @param values a <code>Map</code> of fieldName: value
     * @param index the index where to insert the row, use negative for an index relative to the end of the list
     * @return the created <code>Record</code> object or null if no record could be created
     */
    protected Record addRow(int index, Map<String, Object> values) {
        // allow negative index as relative to the end of the list
        if (index < 0) {
            index = store.getCount() + index;
        }

        if (recordDef != null) {
            List<Object> val = new ArrayList<Object>();
            for (FieldDef fd : recordDef.getFields()) {
                String fn = fd.getName();
                if (values.containsKey(fn)) {
                    val.add(values.get(fn));
                } else {
                    val.add(null);
                }
            }
            Record newRecord = recordDef.createRecord(val.toArray());
            store.insert(index, newRecord);
            return newRecord;
        }
        return null;
    }

    /**
     * Inserts a new row at the end of the list with given values.
     * @param values a <code>Map</code> of fieldName: value
     * @return the created <code>Record</code> object or null if no record could be created
     */
    protected Record addRow(Map<String, Object> values) {
        return addRow(store.getCount(), values);
    }

    /**
     * Handles the save all batch action, this takes all records that need
     * to be created, update or deleted and send them in one request.
     * The posted data is the same format as for a <code>service-multi</code>.
     */
    protected void doBatchAction() {
        UtilUi.logInfo("doBatchAction ...", MODULE, "doBatchAction");
        String data = makeBatchPostData();
        if (data == null) {
            UtilUi.logInfo("nothing to do", MODULE, "doBatchAction");
            return;
        }

        RequestBuilder request = new RequestBuilder(RequestBuilder.POST, GWT.getHostPageBaseURL() + saveAllUrl);
        request.setHeader("Content-type", "application/x-www-form-urlencoded");
        request.setRequestData(data);
        request.setTimeoutMillis(UtilLookup.getAjaxDefaultTimeout());
        request.setCallback(new RequestCallback() {
            public void onError(Request request, Throwable exception) {
                // display error message
                markGridNotBusy();
                UtilUi.errorMessage(exception.toString());
            }

            public void onResponseReceived(Request request, Response response) {
                // if it is a correct response, reload the grid
                markGridNotBusy();
                UtilUi.logInfo("onResponseReceived, response = " + response, MODULE, "doBatchAction");
                if (!ServiceErrorReader.showErrorMessageIfAny(response, saveAllUrl)) {
                    // commit store changes
                    getStore().commitChanges();
                    loadFirstPage();
                }
            }
        });
        try {
            markGridBusy();
            UtilUi.logInfo("posting batch", MODULE, "doBatchAction");
            request.send();
        } catch (RequestException e) {
            // display error message
            UtilUi.errorMessage(e.toString(), MODULE, "doBatchAction");
        }
    }

    private String makeBatchPostData() {
        StringBuilder sb = new StringBuilder();
        int index = 0;
        index = makeBatchPostData(index, UtilLookup.PARAM_CUD_ACTION_CREATE, getRecordsToCreate(), sb);
        index = makeBatchPostData(index, UtilLookup.PARAM_CUD_ACTION_UPDATE, getRecordsToUpdate(), sb);
        index = makeBatchPostData(index, UtilLookup.PARAM_CUD_ACTION_DELETE, getRecordsToDelete(), sb);
        if (index == 0) {
            return null;
        }
        sb.append("&").append("_rowCount=").append(index);
        return sb.toString();
    }

    private int makeBatchPostData(int index, String action, List<Record> records, StringBuilder sb) {
        for (Record record : records) {
            if (index > 0) {
                sb.append("&");
            }
            // set the submit flag
            sb.append(UtilLookup.ROW_SUBMIT_PREFIX).append(index).append("=").append("Y");
            // add the action, so the service knows what to do with the data
            sb.append("&").append(UtilLookup.PARAM_CUD_ACTION).append(UtilLookup.MULTI_ROW_DELIMITER).append(index)
                    .append("=").append(URL.encodeQueryString(action));
            for (String field : record.getFields()) {
                // remove client-side permissions
                if (Permissions.isPermissionField(field)) {
                    continue;
                }
                sb.append("&").append(field).append(UtilLookup.MULTI_ROW_DELIMITER).append(index).append("=");
                if (!record.isEmpty(field)) {
                    sb.append(URL.encodeQueryString(record.getAsString(field)));
                }
            }
            // add additional fields that may be required in the service
            if (additionalBatchData != null) {
                for (String extraField : additionalBatchData.keySet()) {
                    sb.append("&").append(extraField).append(UtilLookup.MULTI_ROW_DELIMITER).append(index)
                            .append("=").append(URL.encodeQueryString(additionalBatchData.get(extraField)));
                }
            }
            index++;
        }
        return index;
    }

    /**
     * Sets the fields that define a <code>Record</code> primary key.
     * The main use is to check if a <code>Record</code> exists, which implies
     * the primary key fields are all non empty.
     * @param fields the list of fields composing the primary key in a <code>Record</code>
     */
    public void setRecordPrimaryKeyFields(Collection<String> fields) {
        recordPrimaryKeyFields = new HashSet<String>();
        recordPrimaryKeyFields.addAll(fields);
    }

    /**
     * Determines if a given <code>Record</code> exists in the application.
     * For example this is used to determine if a record should be posted to be
     * Created or Updated.
     * @param record a <code>Record</code> value
     * @return a <code>boolean</code> value
     */
    protected boolean recordExists(Record record) {
        for (String f : recordPrimaryKeyFields) {
            if (record.isEmpty(f)) {
                return false;
            }
        }
        return true;
    }

    /**
     * Handles the create or update action on the given row, this uses {@link #recordExists}
     * to determine if the action should be a Create or Update.
     * @param record the <code>Record</code> to update or create
     */
    private void doUpdateCreateAction(Record record) {
        if (recordExists(record)) {
            doUpdateAction(record);
        } else {
            doCreateAction(record);
        }
    }

    /**
     * Handles the update action on the given row.
     * Can override to do some immediate action with the <code>Record</code>.
     * @param record the <code>Record</code> to update or create
     */
    protected void doUpdateAction(Record record) {
    }

    /**
     * Handles the create action on the given row.
     * Can override to do some immediate action with the <code>Record</code>.
     * @param record the <code>Record</code> to update or create
     */
    protected void doCreateAction(Record record) {
    }

    /**
     * Handles the delete action on the given row.
     * Can override to do some immediate action with the <code>Record</code>, else the default implementation
     * is to store the <code>Record</code> if it exists for later batch action and removes it from the grid.
     * @param record the <code>Record</code> to delete
     */
    protected void doDeleteAction(Record record) {
        getStore().remove(record);
        if (recordExists(record)) {
            toDeleteRecords.add(record);
        } else {
            // commit the record else the grid will keep it in its cache
            record.commit();
        }
    }

    /**
     * Gets the list of <code>Record</code> that have been marked for deletion.
     * This can be used if the {@link #doDeleteAction} was not overridden to immediately delete the record
     * to do batch delete instead.
     * @return the <code>List</code> of <code>Record</code> that were marked for deletion
     */
    protected List<Record> getRecordsToDelete() {
        for (Record rec : toDeleteRecords) {
            UtilUi.logInfo("To DELETE: " + UtilUi.toString(rec), MODULE, "getRecordsToDelete");
        }

        return toDeleteRecords;
    }

    /**
     * Gets the list of <code>Record</code> that should be created.
     * This can be used to do batch action.
     * @return the <code>List</code> of <code>Record</code> that should be created
     */
    protected List<Record> getRecordsToCreate() {
        List<Record> toCreate = new ArrayList<Record>();
        for (Record rec : getStore().getModifiedRecords()) {
            if (!recordExists(rec)) {
                UtilUi.logInfo("To CREATE: " + UtilUi.toString(rec), MODULE, "getRecordsToCreate");
                toCreate.add(rec);
            }
        }
        return toCreate;
    }

    /**
     * Gets the list of <code>Record</code> that should be updated.
     * This can be used to do batch action.
     * @return the <code>List</code> of <code>Record</code> that should be updated.
     */
    protected List<Record> getRecordsToUpdate() {
        List<Record> toUpdate = new ArrayList<Record>();
        for (Record rec : getStore().getModifiedRecords()) {
            if (recordExists(rec)) {
                UtilUi.logInfo("To UPDATE: " + UtilUi.toString(rec), MODULE, "getRecordsToUpdate");
                toUpdate.add(rec);
            }
        }
        return toUpdate;
    }

    /**
     * Populates the grid rows extra info, this should return the HTML code to insert as a secondary row for a given record.
     * Default implementation returns <code>null</code>.
     * @param record the row <code>Record</code>
     * @param index the row index
     * @return the HTML to include in the extra row, if <code>null</code> or empty it won't be visible
     */
    protected String getRowBody(Record record, int index) {
        return null;
    }

    /**
     * Sets a custom CSS style to a row.
     * Default implementation returns <code>null</code>.
     * @param record the row <code>Record</code>
     * @param index the row index
     * @param extraInfo the extra content if any
     * @return a CSS style
     */
    protected String getRowBodyStyle(Record record, int index, String extraInfo) {
        return null;
    }

    /**
     * Sets a custom CSS class to a row.
     * Default implementation returns <code>null</code>.
     * @param record the row <code>Record</code>
     * @param index the row index
     * @param extraInfo the extra content if any
     * @return a String that is appended to the normal class of the row
     */
    protected String getRowExtraClass(Record record, int index, String extraInfo) {
        return null;
    }

    /**
     * Registers a <code>LoadableListener</code>.
     * @param listener a <code>LoadableListener</code> value
     */
    public void addLoadableListener(LoadableListener listener) {
        listeners.add(listener);
    }

    protected void notifyLoad() {
        loaded = true;
        for (LoadableListener l : listeners) {
            l.onLoad();
        }
    }

    // those two methods are for cell locking / unlocking

    /**
     * Locks a cell with the given coordinates so that it cannot be edited.
     * @param rowIndex an <code>int</code> value
     * @param colIndex an <code>int</code> value
     */
    public void lockCell(int rowIndex, int colIndex) {
        Cell c = new Cell(rowIndex, colIndex);
        UtilUi.logDebug("Locking " + c, MODULE, "lockCell");
        lockedCells.add(c);
    }

    /**
     * Unlocks a cell with the given coordinates so that it can be edited again.
     * @param rowIndex an <code>int</code> value
     * @param colIndex an <code>int</code> value
     */
    public void unlockCell(int rowIndex, int colIndex) {
        Cell c = new Cell(rowIndex, colIndex);
        UtilUi.logDebug("Unlocking " + c, MODULE, "unlockCell");
        lockedCells.remove(c);
    }

    /**
     * Unlocks all locked cell so that they can be edited again.
     */
    public void unlockAllCells() {
        for (Cell c : lockedCells) {
            UtilUi.logDebug("Unlocking " + c, MODULE, "unlockCell");
        }
        lockedCells.clear();
    }

    /**
     * A place holder event handler for cell edition for sub classes.
     * @param record the <code>Record</code> that was modified
     * @param field the <code>String</code> in the record that was modified
     * @param oldValue the field value before it was modified, an <code>Object</code> value
     * @param rowIndex an <code>int</code> value
     * @param colIndex an <code>int</code> value
     */
    public void cellValueChanged(Record record, String field, Object oldValue, int rowIndex, int colIndex) {
    }

    // those method are from the StoreListener interface

    /** {@inheritDoc} */
    public boolean doBeforeLoad(Store store) {
        return true;
    }

    /** {@inheritDoc} */
    public void onAdd(Store store, Record[] records, int index) {
        UtilUi.logInfo("onAdd, index = " + index, MODULE, "onAdd");
        // we have to trigger a resize so the container can expand with the grid
        syncSize();
    }

    /** {@inheritDoc} */
    public void onClear(Store store) {
    }

    /** {@inheritDoc} */
    public void onDataChanged(Store store) {
        UtilUi.logInfo("onDataChanged", MODULE, "onDataChanged");
    }

    /** {@inheritDoc}
     * The default implementation is to automatically add a new record if the permission is set. */
    public void onLoad(Store store, Record[] records) {
        UtilUi.logInfo("onLoad", MODULE, "onLoad");
        // reset the list of records to delete
        toDeleteRecords = new ArrayList<Record>();
        // find the first record that is always included for permissions
        Record globalPermissionsRecord = records[0];
        globalPermissions = new Permissions(globalPermissionsRecord);
        store.remove(globalPermissionsRecord);

        // if the grid is not editable, or if global permissions do not have create / update or delete, hide those columns
        boolean noUpdateCreate = !editable || (!globalPermissions.canCreate() && !globalPermissions.canUpdate());
        boolean noDelete = !editable || !globalPermissions.canDelete();

        UtilUi.logInfo("noUpdateCreate = " + noUpdateCreate + ", noDelete = " + noDelete, MODULE, "onLoad");

        if (createUpdateIndex > 0) {
            getColumnModel().setHidden(createUpdateIndex, noUpdateCreate);
        }

        if (deleteIndex > 0) {
            getColumnModel().setHidden(deleteIndex, noDelete);
        }

        // if cannot create / update and delete, hide the Save all button
        if (saveAllButton != null) {
            if (noUpdateCreate && noDelete) {
                saveAllButton.hide();
            } else {
                saveAllButton.show();
            }
        }

        addRowIfCreatePermission();

        if (useSummaryRow) {
            addSummaryRow();
        }

        // unlock cells
        unlockAllCells();

        // now we are all loaded (internal autocompleters had to be loaded for the grid to load)
        notifyLoad();
    }

    /** {@inheritDoc} */
    public void onLoadException(Throwable error) {
    }

    /** {@inheritDoc} */
    public void onRemove(Store store, Record record, int index) {
    }

    /** {@inheritDoc} */
    public void onUpdate(Store store, Record record, Record.Operation operation) {
        UtilUi.logInfo("onUpdate : " + operation.getOperation() + " : " + UtilUi.toString(record), MODULE,
                "onUpdate");
        // check if we are editing the new record line
        if (operation == Record.EDIT && !recordExists(record)) {
            // if canDelete is already set no need to done anything else
            // else we insert a blank record and set canDelete
            if (!Permissions.canDelete(record)) {
                Permissions.setCanDelete(true, record);
                addRowIfCreatePermission(-1);
            }
        }
    }

    /**
     * Marks the grid as busy.
     * Should be used when some Asynchronous events are running and some action cannot be performed until they are finished.
     */
    public void markGridBusy() {
        UtilUi.logInfo("grid busy", MODULE, "markGridBusy");
        getEl().mask(UtilUi.MSG.loading());
    }

    /**
     * Marks the grid as not busy.
     * Should be used when Asynchronous finished running and actions can be performed on the grid freely.
     */
    public void markGridNotBusy() {
        UtilUi.logInfo("grid not busy", MODULE, "markGridNotBusy");
        getEl().unmask();
    }

    /**
     * Checks if a cell at given coordinates is editable.
     * @param rowIndex an <code>int</code> value
     * @param colIndex an <code>int</code> value
     * @return a <code>boolean</code> value
     */
    public boolean isEditableCell(int rowIndex, int colIndex) {
        // check the grid global flag
        if (!editable) {
            return false;
        }

        // check for locked cell
        Cell c = new Cell(rowIndex, colIndex);
        if (lockedCells.contains(c)) {
            UtilUi.logDebug("Cell is locked " + c, MODULE, "isEditableCell");
            return false;
        }

        // check row (record) permission
        return Permissions.canUpdate(store.getAt(rowIndex));
    }

    /**
     * Checks if a cell at given coordinates is editable. Also this method can be overridden
     * to analyze cell field name and its value. Sometimes we may need to allow/disallow editing
     * based on the existing cell value. By default just calls <code>isEditableCell(int, int)</code>.
     * @param rowIndex an <code>int</code> value
     * @param colIndex an <code>int</code> value
     * @param field a field name
     * @param value this is current value in the cell
     * @return a <code>boolean</code> value
     */
    public boolean isEditableCell(int rowIndex, int colIndex, String field, Object value) {
        return isEditableCell(rowIndex, colIndex);
    }

    /**
     * Gets the row index of the next editable cell for the given column index, starting from below the given current row index..
     * @param currentRow an <code>int</code> value
     * @param cellColIndex an <code>int</code> value
     * @return the row index found, or <code>-1</code> if it reaches the end of the list
     */
    public int getNextEditableCell(int currentRow, int cellColIndex) {
        for (int i = currentRow + 1; i < store.getCount(); i++) {
            if (isEditableCell(i, cellColIndex)) {
                return i;
            }
        }
        return -1;
    }

    /**
     * Starts editing the next editable cell for the given column index, starting from below the given current row index.
     * Does nothing if no editable cell is found.
     * @param currentRow an <code>int</code> value
     * @param cellColIndex an <code>int</code> value
     * @see #getNextEditableCell
     */
    public void startEditingNextEditableCell(int currentRow, int cellColIndex) {
        int i = getNextEditableCell(currentRow, cellColIndex);
        if (i >= 0) {
            stopEditing();
            startEditing(i, cellColIndex);
        }
    }

    protected int getCurrentColumnIndex() {
        return columnConfigs.size();
    }
}