Java tutorial
/* * 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); } }