org.jax.pubarray.gwtqueryapp.client.TermsOfInterestContainer.java Source code

Java tutorial

Introduction

Here is the source code for org.jax.pubarray.gwtqueryapp.client.TermsOfInterestContainer.java

Source

/*
 * Copyright (c) 2010 The Jackson Laboratory
 * 
 * This is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This software 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this software.  If not, see <http://www.gnu.org/licenses/>.
 */

package org.jax.pubarray.gwtqueryapp.client;

import java.util.ArrayList;
import java.util.List;
import java.util.Vector;

import org.jax.gwtutil.client.event.ChangeBroadcaster;
import org.jax.gwtutil.client.event.ChangeListener;
import org.jax.pubarray.gwtcommon.client.MatchesAllFunction;
import org.jax.pubarray.gwtcommon.client.MatchesAnyFunction;
import org.jax.pubarray.gwtcommon.client.QualifiedColumnMetadata;
import org.jax.pubarray.gwtcommon.client.QualifiedColumnMetadataModelUtil;
import org.jax.pubarray.gwtcommon.client.Query;
import org.jax.pubarray.gwtcommon.client.QueryModelUtil;
import org.jax.pubarray.gwtcommon.client.TableColumnMetadata;
import org.jax.pubarray.gwtcommon.client.TableMetadata;

import com.extjs.gxt.ui.client.data.BasePagingLoader;
import com.extjs.gxt.ui.client.data.ModelData;
import com.extjs.gxt.ui.client.data.PagingLoadResult;
import com.extjs.gxt.ui.client.data.PagingLoader;
import com.extjs.gxt.ui.client.event.ButtonEvent;
import com.extjs.gxt.ui.client.event.ComponentEvent;
import com.extjs.gxt.ui.client.event.Events;
import com.extjs.gxt.ui.client.event.Listener;
import com.extjs.gxt.ui.client.event.SelectionListener;
import com.extjs.gxt.ui.client.store.ListStore;
import com.extjs.gxt.ui.client.store.Store;
import com.extjs.gxt.ui.client.store.StoreEvent;
import com.extjs.gxt.ui.client.store.StoreListener;
import com.extjs.gxt.ui.client.store.Record.RecordUpdate;
import com.extjs.gxt.ui.client.widget.ComponentPlugin;
import com.extjs.gxt.ui.client.widget.ContentPanel;
import com.extjs.gxt.ui.client.widget.LayoutContainer;
import com.extjs.gxt.ui.client.widget.TabItem;
import com.extjs.gxt.ui.client.widget.TabPanel;
import com.extjs.gxt.ui.client.widget.button.Button;
import com.extjs.gxt.ui.client.widget.form.SimpleComboBox;
import com.extjs.gxt.ui.client.widget.form.TextField;
import com.extjs.gxt.ui.client.widget.form.ComboBox.TriggerAction;
import com.extjs.gxt.ui.client.widget.grid.CheckColumnConfig;
import com.extjs.gxt.ui.client.widget.grid.ColumnConfig;
import com.extjs.gxt.ui.client.widget.grid.ColumnModel;
import com.extjs.gxt.ui.client.widget.grid.Grid;
import com.extjs.gxt.ui.client.widget.toolbar.LabelToolItem;
import com.extjs.gxt.ui.client.widget.toolbar.PagingToolBar;
import com.extjs.gxt.ui.client.widget.toolbar.SeparatorToolItem;
import com.extjs.gxt.ui.client.widget.toolbar.ToolBar;
import com.google.gwt.event.logical.shared.ResizeEvent;
import com.google.gwt.event.logical.shared.ResizeHandler;
import com.google.gwt.user.client.Command;
import com.google.gwt.user.client.DeferredCommand;
import com.google.gwt.user.client.Element;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.rpc.AsyncCallback;

/**
 * The terms of interest container allows the user to specify which terms
 * (ie columns) they are interested in working with
 * @author <A HREF="mailto:keith.sheppard@jax.org">Keith Sheppard</A>
 */
public class TermsOfInterestContainer extends LayoutContainer
        implements ChangeBroadcaster<TermsOfInterestContainer> {
    private static final int PAGING_ROW_COUNT = 200;

    private static final String MATCH_ALL_TERMS_FILTER = "Match All Terms";
    private static final String MATCH_ANY_TERMS_FILTER = "Match Any Term";

    private final ChangeListener<TermsOfInterestContainer> initializationCompleteListener;
    private final List<ChangeListener<TermsOfInterestContainer>> changeListeners;
    private final List<ChangeListener<TermsOfInterestContainer>> internalMaybeLayoutChangedListeners;

    private final Grid<ModelData> dataGrid;
    private final List<Grid<ModelData>> annotationGrids;

    private TableColumnMetadata[] designColumnMetadata = null;
    private String[][] designData = null;
    private TableMetadata dataMetadata;

    private final String[] annotationCategories;

    // this number is for tracking how many pending load requests there are.
    // Every time we make a load request this number is incremented and
    // every time we get a response it is decremented
    private int pendingLoadRequests = 0;
    private boolean initializationComplete = false;

    // This number represents the total number of terms that have been loaded
    // over all of the grids
    private int loadedTermCount = 0;

    private class TermsOfInterestStoreListener extends StoreListener<ModelData> {
        private final Grid<ModelData> grid;

        /**
         * Constructor
         * @param grid
         *          the grid that the events we're listening to are related to
         */
        public TermsOfInterestStoreListener(Grid<ModelData> grid) {
            this.grid = grid;
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public void storeUpdate(StoreEvent<ModelData> se) {
            TermsOfInterestContainer.this.termsDataStoreUpdated(se, this.grid);
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public void storeDataChanged(StoreEvent<ModelData> se) {
            TermsOfInterestContainer.this.modelsUploaded(se.getStore());
        }
    }

    /**
     * Constructor
     * @param queryService
     *          the query service to use
     * @param annotationCategories
     *          the annotation categories
     * @param initializationCompleteListener
     *          this gets called after initialization is complete
     */
    public TermsOfInterestContainer(final QueryServiceAsync queryService, final String[] annotationCategories,
            final ChangeListener<TermsOfInterestContainer> initializationCompleteListener) {
        this.annotationCategories = annotationCategories;
        this.initializationCompleteListener = initializationCompleteListener;

        this.changeListeners = new ArrayList<ChangeListener<TermsOfInterestContainer>>();
        this.internalMaybeLayoutChangedListeners = new ArrayList<ChangeListener<TermsOfInterestContainer>>();
        final DataMetadataProxy dataProxy = new DataMetadataProxy(false, queryService);
        final PagingLoader<PagingLoadResult<ModelData>> dataStoreLoader = new BasePagingLoader<PagingLoadResult<ModelData>>(
                dataProxy);

        ColumnModel dataColumnModel = this.makeGridColumnModel();
        final ListStore<ModelData> dataStore = new ListStore<ModelData>(dataStoreLoader);
        this.dataGrid = new Grid<ModelData>(dataStore, dataColumnModel);
        this.dataGrid.addPlugin((ComponentPlugin) dataColumnModel.getColumn(0));
        this.dataGrid.setBorders(false);
        this.dataGrid.setHeight(300);
        this.dataGrid.setLoadMask(true);
        this.dataGrid.setAutoWidth(true);
        this.dataGrid.setAutoExpandColumn(CachedPagingProxy.LABEL_PROP_STRING);
        this.dataGrid.setAutoExpandMax(Integer.MAX_VALUE);
        dataStore.addStoreListener(new TermsOfInterestStoreListener(this.dataGrid));

        this.annotationGrids = new ArrayList<Grid<ModelData>>(annotationCategories.length);
        for (int i = 0; i < annotationCategories.length; i++) {
            final AnnotationMetadataProxy proxy = new AnnotationMetadataProxy(annotationCategories[i], false,
                    queryService);
            final PagingLoader<PagingLoadResult<ModelData>> loader = new BasePagingLoader<PagingLoadResult<ModelData>>(
                    proxy);

            final ColumnModel annotationColumnModel = this.makeGridColumnModel();
            final ListStore<ModelData> store = new ListStore<ModelData>(loader);
            final Grid<ModelData> grid = new Grid<ModelData>(store, annotationColumnModel);
            grid.addPlugin((ComponentPlugin) annotationColumnModel.getColumn(0));
            grid.setBorders(false);
            grid.setHeight(300);
            grid.setLoadMask(true);
            grid.setAutoWidth(true);
            grid.setAutoExpandColumn(AnnotationMetadataProxy.LABEL_PROP_STRING);
            grid.setAutoExpandMax(Integer.MAX_VALUE);
            store.addStoreListener(new TermsOfInterestStoreListener(grid));

            this.annotationGrids.add(grid);
        }

        queryService.getDesignTerms(new AsyncCallback<TableColumnMetadata[]>() {
            public void onFailure(Throwable caught) {
                // TODO tell user
                caught.printStackTrace();
            }

            public void onSuccess(TableColumnMetadata[] metadata) {
                TermsOfInterestContainer.this.designColumnMetadataLoaded(metadata);
            }
        });

        queryService.getDesign(new AsyncCallback<String[][]>() {
            public void onFailure(Throwable caught) {
                // TODO tell user
                caught.printStackTrace();
            }

            public void onSuccess(String[][] designData) {
                TermsOfInterestContainer.this.designDataLoaded(designData);
            }
        });

        queryService.getDataTableMetadata(new AsyncCallback<TableMetadata>() {
            /**
             * {@inheritDoc}
             */
            public void onFailure(Throwable caught) {
                // TODO tell the user
                caught.printStackTrace();
            }

            /**
             * {@inheritDoc}
             */
            public void onSuccess(TableMetadata metadata) {
                TermsOfInterestContainer.this.dataTableMetadataLoaded(metadata);
            }
        });
    }

    /**
     * Determines if initialization is complete
     * @return true iff initialization is complete
     */
    public boolean isInitializationComplete() {
        return this.initializationComplete;
    }

    private ColumnModel makeGridColumnModel() {
        List<ColumnConfig> colConfigs = new ArrayList<ColumnConfig>();
        CheckColumnConfig selectColumn = new CheckColumnConfig(QueryModelUtil.SELECT_COLUMN_ID, "Select", 75);
        colConfigs.add(selectColumn);
        colConfigs.add(new ColumnConfig(CachedPagingProxy.LABEL_PROP_STRING, "Term Name", 400));
        for (ColumnConfig colConfig : colConfigs) {
            colConfig.setSortable(false);
            colConfig.setMenuDisabled(true);
        }

        return new ColumnModel(colConfigs);
    }

    private void dataTableMetadataLoaded(TableMetadata metadata) {
        this.dataMetadata = metadata;
        this.maybeInitializeArrayFilterAndLoadGrid();
    }

    private void designDataLoaded(String[][] designData) {
        this.designData = designData;
        this.maybeInitializeArrayFilterAndLoadGrid();
    }

    private void designColumnMetadataLoaded(TableColumnMetadata[] designColumnMetadata) {
        this.designColumnMetadata = designColumnMetadata;
        this.maybeInitializeArrayFilterAndLoadGrid();
    }

    private void maybeInitializeArrayFilterAndLoadGrid() {
        if (this.designData != null && this.designColumnMetadata != null && this.dataMetadata != null) {
            System.out.println("ready to initialize array filter");
            this.loadGrids();
        }
    }

    private void loadGrids() {
        // kick off loading all of the grids
        // TODO this should probably be incremental... all at once may
        // overwhelm some browsers
        this.pendingLoadRequests = 1 + this.annotationGrids.size();
        this.dataGrid.getStore().getLoader().load();
        for (Grid<ModelData> annotationGrid : this.annotationGrids) {
            annotationGrid.getStore().getLoader().load();
        }

        if (this.initializationCompleteListener != null) {
            this.initializationCompleteListener.changeOccured(this);
        }
    }

    /**
     * Lets this container know that the layout might have changed so that
     * we can change panel sizes if needed
     */
    public void maybeLayoutChanged() {
        for (final ChangeListener<TermsOfInterestContainer> layoutListener : this.internalMaybeLayoutChangedListeners) {
            // defer the layout updates in order to allow the browser breathing room
            DeferredCommand.addCommand(new Command() {
                /**
                 * {@inheritDoc}
                 */
                public void execute() {
                    layoutListener.changeOccured(TermsOfInterestContainer.this);
                }
            });
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected void onRender(Element parent, int index) {
        super.onRender(parent, index);

        final TabPanel tabPanel = new TabPanel();
        tabPanel.setPlain(true);
        tabPanel.setAutoHeight(true);

        for (int i = 0; i < this.annotationCategories.length; i++) {
            this.addGridTab(tabPanel, this.annotationCategories[i], this.annotationGrids.get(i));
        }
        this.addGridTab(tabPanel, "Data Columns", this.dataGrid);
        this.add(tabPanel);
    }

    private void addGridTab(final TabPanel tabPanel, final String tabName, final Grid<ModelData> grid) {
        final TabItem tabItem = new TabItem(tabName);
        tabItem.setAutoWidth(true);

        final ContentPanel contentPanel = new ContentPanel();
        contentPanel.add(grid);
        contentPanel.setHeaderVisible(false);
        contentPanel.setBorders(false);

        ToolBar topToolBar = new ToolBar();
        PagingToolBar bottomToolBar = new PagingToolBar(PAGING_ROW_COUNT);
        this.addToolbarItems(topToolBar, bottomToolBar, grid);
        contentPanel.setTopComponent(topToolBar);
        contentPanel.setBottomComponent(bottomToolBar);

        tabItem.add(contentPanel);
        tabPanel.add(tabItem);
        tabItem.addListener(Events.Select, new Listener<ComponentEvent>() {
            /**
             * {@inheritDoc}
             */
            public void handleEvent(ComponentEvent be) {
                if (tabItem.isRendered()) {
                    System.out.println(tabName + " tab selected");
                    grid.getView().refresh(true);
                }
            }
        });

        final Command resizeTabCommand = new Command() {
            /**
             * {@inheritDoc}
             */
            public void execute() {
                // getWidth is not allowed until a component is
                // rendered
                if (tabItem.isRendered()) {
                    grid.syncSize();
                }
            }
        };

        // if we don't have this then the grid doesn't resize correctly when the
        // browser window is resized
        Window.addResizeHandler(new ResizeHandler() {
            /**
             * {@inheritDoc}
             */
            public void onResize(ResizeEvent event) {
                // defer the command to give the browser a chance
                // to deal with any other events
                DeferredCommand.addCommand(resizeTabCommand);
            }
        });

        this.internalMaybeLayoutChangedListeners.add(new ChangeListener<TermsOfInterestContainer>() {
            /**
             * {@inheritDoc}
             */
            public void changeOccured(TermsOfInterestContainer source) {
                // defer the command to give the browser a chance
                // to deal with any other events
                DeferredCommand.addCommand(resizeTabCommand);
            }
        });
    }

    /**
     * Adds the filter stuff allong with the "Toggle Select All" button
     * to the toolbar
     * @param topToolbar
     *          the toolbar
     * @param bottomToolBar
     *          the paging toolbar
     * @param grid
     *          the grid
     */
    @SuppressWarnings("unchecked")
    private void addToolbarItems(final ToolBar topToolbar, final PagingToolBar bottomToolBar,
            final Grid<ModelData> grid) {
        PagingLoader pagingLoader = (PagingLoader) grid.getStore().getLoader();
        bottomToolBar.bind(pagingLoader);

        final MatchesAllFunction<ModelData> matchesAllFunction = new MatchesAllFunction<ModelData>();
        final MatchesAnyFunction<ModelData> matchesAnyFunction = new MatchesAnyFunction<ModelData>();
        final CachedPagingProxy proxy = this.extractProxy(grid);

        proxy.setFilterFunction(matchesAllFunction);

        topToolbar.add(new LabelToolItem("Filter:"));
        final TextField<String> filterText = new TextField<String>();
        filterText.setEmptyText("Eg: term1 term2 ...");
        topToolbar.add(filterText);

        final SimpleComboBox<String> filterChoiceCombo = new SimpleComboBox<String>();
        filterChoiceCombo.setWidth(120);
        filterChoiceCombo.setAllowBlank(false);
        filterChoiceCombo.setEditable(false);
        filterChoiceCombo.add(MATCH_ALL_TERMS_FILTER);
        filterChoiceCombo.add(MATCH_ANY_TERMS_FILTER);
        filterChoiceCombo.setSimpleValue(MATCH_ALL_TERMS_FILTER);
        filterChoiceCombo.setTriggerAction(TriggerAction.ALL);
        topToolbar.add(filterChoiceCombo);
        topToolbar.add(new SeparatorToolItem());

        Button runFilterButton = new Button("Run Filter");
        runFilterButton.addSelectionListener(new SelectionListener<ButtonEvent>() {
            /**
             * {@inheritDoc}
             */
            @Override
            public void componentSelected(ButtonEvent ce) {
                String filterValueString = filterText.getValue();
                String filterTypeString = filterChoiceCombo.getSimpleValue();
                if (filterTypeString == MATCH_ALL_TERMS_FILTER) {
                    matchesAllFunction.updateDelimitedText(filterValueString);
                    proxy.setFilterFunction(matchesAllFunction);
                } else {
                    matchesAnyFunction.updateDelimitedText(filterValueString);
                    proxy.setFilterFunction(matchesAnyFunction);
                }

                TermsOfInterestContainer.this.reload(bottomToolBar);
                TermsOfInterestContainer.this.fireChangeEvent();
            }
        });
        topToolbar.add(runFilterButton);
        topToolbar.add(new SeparatorToolItem());

        Button selectAllButton = new Button("Toggle Select All");
        selectAllButton.addSelectionListener(new SelectionListener<ButtonEvent>() {
            /**
             * {@inheritDoc}
             */
            @Override
            public void componentSelected(ButtonEvent ce) {
                TermsOfInterestContainer.this.toggleSelectAllFromGrid(grid);
            }
        });

        topToolbar.add(selectAllButton);
        topToolbar.add(new SeparatorToolItem());
    }

    private void reload(final PagingToolBar bottomToolBar) {
        DeferredCommand.addCommand(new Command() {
            /**
             * {@inheritDoc}
             */
            public void execute() {
                if (bottomToolBar.getActivePage() != 1) {
                    bottomToolBar.setActivePage(1);
                } else {
                    bottomToolBar.refresh();
                }
            }
        });
    }

    /**
     * Called when the user clicks "toggle select all" for a grid. This function
     * only operates on the terms that pass through the user's specified filter
     * (assuming there is one). This function fires a change event
     * @param grid
     *          the grid
     */
    private void toggleSelectAllFromGrid(final Grid<ModelData> grid) {
        boolean anyAreNotSelected = false;
        List<ModelData> filteredModels = this.extractProxy(grid).getFilteredModels();
        for (ModelData currModel : filteredModels) {
            if (!QueryModelUtil.isSelected(currModel)) {
                anyAreNotSelected = true;
                break;
            }
        }

        // if any are not selected then we should select all. otherwise
        // we should deselect all
        for (ModelData currModel : filteredModels) {
            currModel.set(QueryModelUtil.SELECT_COLUMN_ID, anyAreNotSelected);
        }

        grid.getView().refresh(false);
        this.fireChangeEvent();
    }

    /**
     * Select all from the given grid. This unlike
     * {@link #toggleSelectAllFromGrid(Grid)} this function does not care
     * if a term is filtered or not. It gets selected either way. Also
     * this function will not fire a change event
     * @param grid  the grid to select
     */
    private void selectAllFromGrid(final Grid<ModelData> grid) {
        List<ModelData> models = grid.getStore().getModels();
        for (ModelData currModel : models) {
            currModel.set(QueryModelUtil.SELECT_COLUMN_ID, true);
        }

        grid.getView().refresh(false);
    }

    /**
     * Get the terms of interest selected by the user
     * @return
     *          the terms of interest
     */
    public List<QualifiedColumnMetadata> getSelectedTerms() {
        Vector<QualifiedColumnMetadata> selectedTerms = new Vector<QualifiedColumnMetadata>();

        // the 1st column from the data table is probe ID and it should always
        // be a part of the terms of interest
        if (this.dataMetadata != null && this.dataMetadata.getColumnMetadata() != null
                && this.dataMetadata.getColumnMetadata().length >= 1) {
            TableColumnMetadata probeIdCol = this.dataMetadata.getColumnMetadata()[0];
            selectedTerms.add(new QualifiedColumnMetadata(this.dataMetadata.getTableName(), probeIdCol));
        }

        for (Grid<ModelData> annotationGrid : this.annotationGrids) {
            List<ModelData> filteredAnnoModels = this.extractProxy(annotationGrid).getFilteredModels();
            for (ModelData currAnnoModel : filteredAnnoModels) {
                // filter out models that are not selected
                if (QueryModelUtil.isSelected(currAnnoModel)) {
                    selectedTerms.add(QualifiedColumnMetadataModelUtil.fromModelToPojo(currAnnoModel));
                }
            }
        }

        selectedTerms.addAll(this.getSelectedArrays());

        return selectedTerms;
    }

    private List<QualifiedColumnMetadata> getSelectedArrays() {
        List<QualifiedColumnMetadata> arraysOfInterest = new ArrayList<QualifiedColumnMetadata>();
        List<ModelData> filteredDataModels = this.extractProxy(this.dataGrid).getFilteredModels();
        for (ModelData currDataModel : filteredDataModels) {
            if (QueryModelUtil.isSelected(currDataModel)) {
                QualifiedColumnMetadata currTermOfInterest = QualifiedColumnMetadataModelUtil
                        .fromModelToPojo(currDataModel);
                arraysOfInterest.add(currTermOfInterest);
            }
        }

        return arraysOfInterest;
    }

    /**
     * Extract the proxy from the given model data.
     * @param grid
     *          the grid
     * @return
     *          the proxy
     * @throws ClassCastException
     *          if the grid doesn't have a {@link CachedPagingProxy} tucked
     *          inside a {@link BasePagingLoader}
     */
    @SuppressWarnings("unchecked")
    private CachedPagingProxy extractProxy(Grid<ModelData> grid) throws ClassCastException {
        BasePagingLoader loader = (BasePagingLoader) grid.getStore().getLoader();
        return (CachedPagingProxy) loader.getProxy();
    }

    /**
     * This function is called each time a grid's store is successfully loaded
     * @param store
     *          the store that has been loaded
     */
    private void modelsUploaded(Store<? extends ModelData> store) {
        List<? extends ModelData> models = store.getModels();
        if (models != null) {
            System.out.println("loading models into map");
            this.pendingLoadRequests--;
            this.loadedTermCount += models.size();

            boolean fireChange = false;
            for (ModelData currModel : models) {
                // determines if any of the models are preselected
                if (QueryModelUtil.isSelected(currModel)) {
                    fireChange = true;
                    break;
                }
            }

            // if we have finished loading all terms
            if (this.pendingLoadRequests == 0) {
                // the + 1 adjusts for the probeset ID
                if (this.loadedTermCount + 1 <= Query.MAX_PERMITTED_TERMS) {
                    // we have finished loading all terms and the term count is
                    // less than the maximum so we should just select very term
                    // by default
                    this.selectAllFromGrid(this.dataGrid);
                    for (Grid<ModelData> annoGrid : this.annotationGrids) {
                        this.selectAllFromGrid(annoGrid);
                    }

                    fireChange = true;
                } else {
                    int dataRowCount = this.dataGrid.getStore().getModels().size();

                    // the + 1 adjusts for the probeset ID
                    if (dataRowCount + 1 <= Query.MAX_PERMITTED_TERMS) {
                        // there are too many terms in total but at least we
                        // can select the data terms
                        this.selectAllFromGrid(this.dataGrid);
                        fireChange = true;
                    }
                }

                this.initializationComplete = true;
            }

            store.filter(null);

            // we need to tell the user if there were selection updates
            if (fireChange) {
                System.out.println("firing change for preselected models");
                this.fireChangeEvent();
            }
        } else {
            System.out.println("models are null!");
        }
    }

    /**
     * This function is called each time a grid is selected/deselected
     * @param se
     *          the event
     * @param grid
     *          the affected grid
     */
    private void termsDataStoreUpdated(StoreEvent<ModelData> se, Grid<ModelData> grid) {
        // we don't really care about the commit actions
        if (se.getOperation() != RecordUpdate.COMMIT) {
            System.out.println("terms of interest store update");

            se.getStore().commitChanges();
            this.fireChangeEvent();
        }
    }

    /**
     * Register listeners for changes to the comparison expression
     * @param changeListener
     *          the listener to register
     */
    public void addChangeListener(ChangeListener<TermsOfInterestContainer> changeListener) {
        this.changeListeners.add(changeListener);
    }

    /**
     * Remove listener
     * @see #addChangeListener(ChangeListener)
     * @param changeListener
     *          the change listener to remove from the listener list
     */
    public void removeChangeListener(ChangeListener<TermsOfInterestContainer> changeListener) {
        this.changeListeners.remove(changeListener);
    }

    /**
     * Fire a new change event
     */
    private void fireChangeEvent() {
        DeferredCommand.addCommand(new Command() {
            /**
             * {@inheritDoc}
             */
            public void execute() {
                for (ChangeListener<TermsOfInterestContainer> changeListener : TermsOfInterestContainer.this.changeListeners) {
                    changeListener.changeOccured(TermsOfInterestContainer.this);
                }
            }
        });
    }
}