com.google.gwt.sample.expenses.client.ExpenseList.java Source code

Java tutorial

Introduction

Here is the source code for com.google.gwt.sample.expenses.client.ExpenseList.java

Source

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

import com.google.gwt.cell.client.AbstractCell;
import com.google.gwt.cell.client.Cell;
import com.google.gwt.cell.client.DateCell;
import com.google.gwt.cell.client.TextCell;
import com.google.gwt.cell.client.ValueUpdater;
import com.google.gwt.core.client.GWT;
import com.google.gwt.dom.client.Element;
import com.google.gwt.event.dom.client.BlurEvent;
import com.google.gwt.event.dom.client.BlurHandler;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.dom.client.FocusEvent;
import com.google.gwt.event.dom.client.FocusHandler;
import com.google.gwt.event.dom.client.KeyCodes;
import com.google.gwt.event.dom.client.KeyUpEvent;
import com.google.gwt.event.dom.client.KeyUpHandler;
import com.google.gwt.event.shared.EventBus;
import com.google.gwt.i18n.client.DateTimeFormat;
import com.google.gwt.regexp.shared.RegExp;
import com.google.gwt.requestfactory.shared.EntityProxyChange;
import com.google.gwt.requestfactory.shared.EntityProxyId;
import com.google.gwt.requestfactory.shared.Receiver;
import com.google.gwt.safehtml.shared.SafeHtmlBuilder;
import com.google.gwt.safehtml.shared.SafeHtmlUtils;
import com.google.gwt.sample.expenses.client.request.EmployeeProxy;
import com.google.gwt.sample.expenses.client.request.ExpensesRequestFactory;
import com.google.gwt.sample.expenses.client.request.ReportProxy;
import com.google.gwt.sample.expenses.client.style.Styles;
import com.google.gwt.uibinder.client.UiBinder;
import com.google.gwt.uibinder.client.UiFactory;
import com.google.gwt.uibinder.client.UiField;
import com.google.gwt.user.cellview.client.CellTable;
import com.google.gwt.user.cellview.client.Column;
import com.google.gwt.user.cellview.client.SimplePager;
import com.google.gwt.user.cellview.client.HasKeyboardSelectionPolicy.KeyboardSelectionPolicy;
import com.google.gwt.user.cellview.client.SimplePager.TextLocation;
import com.google.gwt.user.client.Timer;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.Image;
import com.google.gwt.user.client.ui.TextBox;
import com.google.gwt.user.client.ui.Widget;
import com.google.gwt.view.client.AsyncDataProvider;
import com.google.gwt.view.client.HasData;
import com.google.gwt.view.client.NoSelectionModel;
import com.google.gwt.view.client.ProvidesKey;
import com.google.gwt.view.client.Range;
import com.google.gwt.view.client.SelectionChangeEvent;

import java.util.ArrayList;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
 * The list of expense reports on the right side of the app.
 */
public class ExpenseList extends Composite implements EntityProxyChange.Handler<ReportProxy> {

    interface ExpenseListUiBinder extends UiBinder<Widget, ExpenseList> {
    }

    /**
     * Custom listener for this widget.
     */
    interface Listener {

        /**
         * Called when the user selects a report.
         *
         * @param report the selected report
         */
        void onReportSelected(ReportProxy report);
    }

    /**
     * The resources applied to the table.
     */
    interface TableResources extends CellTable.Resources {
        @Source({ CellTable.Style.DEFAULT_CSS, "ExpenseListCellTable.css" })
        TableStyle cellTableStyle();
    }

    /**
     * The styles applied to the table.
     */
    interface TableStyle extends CellTable.Style {
    }

    /**
     * A text box that displays default text.
     */
    private static class DefaultTextBox extends TextBox {

        /**
         * The text color used when the box is disabled and empty.
         */
        private static final String TEXTBOX_DISABLED_COLOR = "#aaaaaa";

        private final String defaultText;

        public DefaultTextBox(final String defaultText) {
            this.defaultText = defaultText;
            resetDefaultText();

            // Add focus and blur handlers.
            addFocusHandler(new FocusHandler() {
                public void onFocus(FocusEvent event) {
                    getElement().getStyle().clearColor();
                    if (defaultText.equals(getText())) {
                        setText("");
                    }
                }
            });
            addBlurHandler(new BlurHandler() {
                public void onBlur(BlurEvent event) {
                    if ("".equals(getText())) {
                        resetDefaultText();
                    }
                }
            });
        }

        public String getDefaultText() {
            return defaultText;
        }

        /**
         * Reset the text box to the default text.
         */
        public void resetDefaultText() {
            setText(defaultText);
            getElement().getStyle().setColor(TEXTBOX_DISABLED_COLOR);
        }
    }

    /**
     * A cell used to highlight search text.
     */
    private class HighlightCell extends AbstractCell<String> {

        private static final String replaceString = "<span style='color:red;font-weight:bold;'>$1</span>";

        @Override
        public void render(String value, Object viewData, SafeHtmlBuilder sb) {
            if (value != null) {
                if (searchRegExp != null) {
                    // The search regex has already been html-escaped
                    value = searchRegExp.replace(SafeHtmlUtils.htmlEscape(value), replaceString);
                    sb.append(SafeHtmlUtils.fromTrustedString(value));
                } else {
                    sb.appendEscaped(value);
                }
            }
        }
    }

    /**
     * The data provider used to retrieve reports.
     */
    private class ReportDataProvider extends AsyncDataProvider<ReportProxy> {

        ReportDataProvider(ProvidesKey<ReportProxy> keyProvider) {
            super(keyProvider);
        }

        @Override
        protected void onRangeChanged(HasData<ReportProxy> display) {
            requestReports(false);
        }
    }

    /**
     * The auto refresh interval in milliseconds.
     */
    private static final int REFRESH_INTERVAL = 5000;

    private static ExpenseListUiBinder uiBinder = GWT.create(ExpenseListUiBinder.class);

    /**
     * Utility method to get the first part of the breadcrumb based on the
     * department and employee.
     *
     * @param department the selected department
     * @param employee the selected employee
     * @return the breadcrumb
     */
    public static String getBreadcrumb(String department, EmployeeProxy employee) {
        if (employee != null) {
            return "Reports for " + employee.getDisplayName();
        } else if (department != null) {
            return "Reports for " + department;
        } else {
            return "All Reports";
        }
    }

    @UiField
    Element breadcrumb;

    @UiField
    SimplePager pager;
    @UiField(provided = true)
    DefaultTextBox searchBox;
    @UiField
    Image searchButton;
    /**
     * The main table. We provide this in the constructor before calling
     * {@link UiBinder#createAndBindUi(Object)} because the pager depends on it.
     */
    @UiField(provided = true)
    CellTable<ReportProxy> table;

    private final List<SortableHeader> allHeaders = new ArrayList<SortableHeader>();

    /**
     * The department being searched.
     */
    private String department;

    /**
     * The employee being searched.
     */
    private EmployeeProxy employee;

    /**
     * Indicates that the report count is stale.
     */
    private boolean isCountStale = true;

    /**
     * The field to sort by.
     */
    private String orderBy = "purpose";

    /**
     * The set of Report keys that we have seen. When a new key is added, we
     * compare it to the list of known keys to determine if it is new.
     */
    private Set<Object> knownReportKeys = null;

    /**
     * Keep track of the last receiver so that we know if a response is stale.
     */
    private Receiver<List<ReportProxy>> lastDataReceiver;

    /**
     * Keep track of the last receiver so that we know if a response is stale.
     */
    private Receiver<Long> lastDataSizeReceiver;

    private Listener listener;

    /**
     * The {@link Timer} used to periodically refresh the table.
     */
    private final Timer refreshTimer = new Timer() {
        @Override
        public void run() {
            isCountStale = true;
            requestReports(true);
        }
    };

    /**
     * The columns to request with each report.
     */
    private final String[] reportColumns = new String[] { "created", "purpose", "notes" };

    /**
     * The data provider that provides reports.
     */
    private final ReportDataProvider reports = new ReportDataProvider(Expenses.REPORT_RECORD_KEY_PROVIDER);

    /**
     * The factory used to send requests.
     */
    private ExpensesRequestFactory requestFactory;

    /**
     * The string that the user searched for.
     */
    private RegExp searchRegExp;

    /**
     * The starts with search string.
     */
    private String startsWithSearch;

    public ExpenseList() {
        // Initialize the widget.
        createTable();
        searchBox = new DefaultTextBox("search");
        initWidget(uiBinder.createAndBindUi(this));

        // Add the view to the data provider.
        reports.addDataDisplay(table);

        // Listen for key events from the text boxes.
        searchBox.addKeyUpHandler(new KeyUpHandler() {
            public void onKeyUp(KeyUpEvent event) {
                // Search on enter.
                if (event.getNativeKeyCode() == KeyCodes.KEY_ENTER) {
                    search();
                    return;
                }

                // Highlight as the user types.
                String text = SafeHtmlUtils.htmlEscape(searchBox.getText());
                if (text.length() > 0) {
                    searchRegExp = RegExp.compile("(" + text + ")", "ig");
                } else {
                    searchRegExp = null;
                }
                table.redraw();
            }
        });
        searchButton.addClickHandler(new ClickHandler() {
            public void onClick(ClickEvent event) {
                search();
            }
        });
    }

    public void init(ExpensesRequestFactory factory, EventBus eventBus) {
        EntityProxyChange.registerForProxyType(eventBus, ReportProxy.class, this);
        this.requestFactory = factory;
        requestReports(false);
    }

    public void onProxyChange(EntityProxyChange<ReportProxy> event) {
        EntityProxyId<ReportProxy> changedId = event.getProxyId();
        List<ReportProxy> records = table.getDisplayedItems();
        int i = 0;
        for (ReportProxy record : records) {
            if (record != null && changedId.equals(record.stableId())) {
                List<ReportProxy> changedList = new ArrayList<ReportProxy>();
                changedList.add(record);
                reports.updateRowData(i + table.getPageStart(), changedList);
            }
            i++;
        }
    }

    /**
     * Set the current department and employee to filter on.
     *
     * @param department the department, or null if none selected
     * @param employee the employee, or null if none selected
     */
    public void setEmployee(String department, EmployeeProxy employee) {
        this.department = department;
        this.employee = employee;
        isCountStale = true;
        searchBox.resetDefaultText();
        startsWithSearch = null;
        breadcrumb.setInnerText(getBreadcrumb(department, employee));
        searchRegExp = null;

        // Refresh the table.
        pager.setPageStart(0);
        requestReports(false);
    }

    public void setListener(Listener listener) {
        this.listener = listener;
    }

    @UiFactory
    SimplePager createPager() {
        SimplePager p = new SimplePager(TextLocation.RIGHT);
        p.setDisplay(table);
        p.setRangeLimited(true);
        return p;
    }

    /**
     * Add a sortable column to the table.
     *
     * @param <C> the data type for the column
     * @param text the header text
     * @param cell the cell used to render the column
     * @param getter the getter to retrieve the value for the column
     * @param property the property to sort by
     * @return the column
     */
    private <C> Column<ReportProxy, C> addColumn(final String text, final Cell<C> cell,
            final GetValue<ReportProxy, C> getter, final String property) {
        final Column<ReportProxy, C> column = new Column<ReportProxy, C>(cell) {
            @Override
            public C getValue(ReportProxy object) {
                return getter.getValue(object);
            }
        };
        final SortableHeader header = new SortableHeader(text);
        allHeaders.add(header);

        // Sort created by default.
        if ("created".equals(property)) {
            header.setSorted(true);
            header.setReverseSort(true);
            orderBy = "created" + " DESC";
        }

        header.setUpdater(new ValueUpdater<String>() {
            public void update(String value) {
                header.setSorted(true);
                header.toggleReverseSort();

                for (SortableHeader otherHeader : allHeaders) {
                    if (otherHeader != header) {
                        otherHeader.setSorted(false);
                        otherHeader.setReverseSort(true);
                    }
                }
                table.redrawHeaders();

                // Request sorted rows.
                orderBy = property;
                if (header.getReverseSort()) {
                    orderBy += " DESC";
                }
                searchBox.resetDefaultText();
                searchRegExp = null;

                // Go to the first page of the newly-sorted results
                pager.firstPage();
                requestReports(false);
            }
        });
        table.addColumn(column, header);
        return column;
    }

    /**
     * Create the {@link CellTable}.
     */
    private void createTable() {
        CellTable.Resources resources = GWT.create(TableResources.class);
        table = new CellTable<ReportProxy>(20, resources);
        table.setKeyboardSelectionPolicy(KeyboardSelectionPolicy.DISABLED);
        Styles.Common common = Styles.common();
        table.addColumnStyleName(0, common.spacerColumn());
        table.addColumnStyleName(1, common.expenseListPurposeColumn());
        table.addColumnStyleName(3, common.expenseListDepartmentColumn());
        table.addColumnStyleName(4, common.expenseListCreatedColumn());
        table.addColumnStyleName(5, common.spacerColumn());

        // Add a selection model.
        final NoSelectionModel<ReportProxy> selectionModel = new NoSelectionModel<ReportProxy>();
        table.setSelectionModel(selectionModel);
        selectionModel.addSelectionChangeHandler(new SelectionChangeEvent.Handler() {
            public void onSelectionChange(SelectionChangeEvent event) {
                Object selected = selectionModel.getLastSelectedObject();
                if (selected != null && listener != null) {
                    listener.onReportSelected((ReportProxy) selected);
                }
            }
        });

        // Spacer column.
        table.addColumn(new SpacerColumn<ReportProxy>());

        // Purpose column.
        addColumn("Purpose", new HighlightCell(), new GetValue<ReportProxy, String>() {
            public String getValue(ReportProxy object) {
                return object.getPurpose();
            }
        }, "purpose");

        // Notes column.
        addColumn("Notes", new HighlightCell(), new GetValue<ReportProxy, String>() {
            public String getValue(ReportProxy object) {
                return object.getNotes();
            }
        }, "notes");

        // Department column.
        addColumn("Department", new TextCell(), new GetValue<ReportProxy, String>() {
            public String getValue(ReportProxy object) {
                return object.getDepartment();
            }
        }, "department");

        // Created column.
        addColumn("Created", new DateCell(DateTimeFormat.getFormat("MMM dd yyyy")),
                new GetValue<ReportProxy, Date>() {
                    public Date getValue(ReportProxy object) {
                        return object.getCreated();
                    }
                }, "created");

        // Spacer column.
        table.addColumn(new SpacerColumn<ReportProxy>());
    }

    /**
     * Send a request for reports in the current range.
     *
     * @param isPolling true if this request is caused by polling
     */
    private void requestReports(boolean isPolling) {
        // Cancel the refresh timer.
        refreshTimer.cancel();

        // Early exit if we don't have a request factory to request from.
        if (requestFactory == null) {
            return;
        }

        // Clear the known keys.
        if (!isPolling) {
            knownReportKeys = null;
        }

        // Get the parameters.
        String startsWith = startsWithSearch;
        if (startsWith == null || searchBox.getDefaultText().equals(startsWith)) {
            startsWith = "";
        }
        Range range = table.getVisibleRange();
        Long employeeId = employee == null ? -1 : new Long(employee.getId());
        String dept = department == null ? "" : department;

        // If a search string is specified, the results will not be sorted.
        if (startsWith.length() > 0) {
            for (SortableHeader header : allHeaders) {
                header.setSorted(false);
                header.setReverseSort(false);
            }
            table.redrawHeaders();
        }

        // Request the total data size.
        if (isCountStale) {
            isCountStale = false;
            if (!isPolling) {
                pager.startLoading();
            }
            lastDataSizeReceiver = new Receiver<Long>() {
                @Override
                public void onSuccess(Long response) {
                    if (this == lastDataSizeReceiver) {
                        int count = response.intValue();
                        // Treat count == 1000 as inexact due to AppEngine limitation
                        reports.updateRowCount(count, count != 1000);
                    }
                }
            };
            requestFactory.reportRequest().countReportsBySearch(employeeId, dept, startsWith)
                    .fire(lastDataSizeReceiver);
        }

        // Request reports in the current range.
        lastDataReceiver = new Receiver<List<ReportProxy>>() {
            @Override
            public void onSuccess(List<ReportProxy> newValues) {
                if (this == lastDataReceiver) {
                    int size = newValues.size();
                    if (size < table.getPageSize()) {
                        // Now we know the exact data size
                        reports.updateRowCount(table.getPageStart() + size, true);
                    }
                    if (size > 0) {
                        reports.updateRowData(table.getPageStart(), newValues);
                    }

                    // Add the new keys to the known keys.
                    boolean isInitialData = knownReportKeys == null;
                    if (knownReportKeys == null) {
                        knownReportKeys = new HashSet<Object>();
                    }
                    for (ReportProxy value : newValues) {
                        Object key = reports.getKey(value);
                        if (!isInitialData && !knownReportKeys.contains(key)) {
                            (new PhaseAnimation.CellTablePhaseAnimation<ReportProxy>(table, value, reports)).run();
                        }
                        knownReportKeys.add(key);
                    }
                }
                refreshTimer.schedule(REFRESH_INTERVAL);
            }
        };

        requestFactory.reportRequest().findReportEntriesBySearch(employeeId, dept, startsWith, orderBy,
                range.getStart(), range.getLength()).with(reportColumns).fire(lastDataReceiver);
    }

    /**
     * Search based on the search box text.
     */
    private void search() {
        isCountStale = true;
        startsWithSearch = searchBox.getText();
        requestReports(false);
    }
}