package __TOP_LEVEL_PACKAGE__.client.scaffold.place;
import com.google.gwt.activity.shared.Activity;
import com.google.gwt.event.shared.EventBus;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.place.shared.Place;
import com.google.gwt.place.shared.PlaceChangeEvent;
import com.google.gwt.place.shared.PlaceController;
import com.google.web.bindery.requestfactory.shared.*;
import com.google.gwt.user.cellview.client.AbstractHasData;
import com.google.gwt.user.client.ui.AcceptsOneWidget;
import com.google.gwt.view.client.*;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Abstract activity for displaying a list of {@link EntityProxy}. These
* activities are not re-usable. Once they are stopped, they cannot be
* restarted.
* <p/>
* Subclasses must:
* <p/>
* <ul>
* <li>provide a {@link ProxyListView}
* <li>implement method to request a full count
* <li>implement method to find a range of entities
* <li>respond to "show details" commands
* </ul>
* <p/>
* Only the properties required by the view will be requested.
*
* @param <P> the type of {@link EntityProxy} listed
*/
public abstract class AbstractProxyListActivity<P extends EntityProxy> implements Activity, ProxyListView.Delegate<P> {
/**
* This mapping allows us to update individual rows as records change.
*/
private final Map<EntityProxyId<P>, Integer> idToRow = new HashMap<EntityProxyId<P>, Integer>();
private final Map<EntityProxyId<P>, P> idToProxy = new HashMap<EntityProxyId<P>, P>();
private final PlaceController placeController;
private final SingleSelectionModel<P> selectionModel;
private final Class<P> proxyClass;
private HandlerRegistration rangeChangeHandler;
private ProxyListView<P> view;
private AcceptsOneWidget display;
private EntityProxyId<P> pendingSelection;
public AbstractProxyListActivity(PlaceController placeController, ProxyListView<P> view, Class<P> proxyType) {
this.view = view;
this.placeController = placeController;
this.proxyClass = proxyType;
view.setDelegate(this);
final HasData<P> hasData = view.asHasData();
rangeChangeHandler = hasData.addRangeChangeHandler(new RangeChangeEvent.Handler() {
public void onRangeChange(RangeChangeEvent event) {
AbstractProxyListActivity.this.onRangeChanged(hasData);
}
});
// Inherit the view's key provider
ProvidesKey<P> keyProvider = ((AbstractHasData<P>) hasData).getKeyProvider();
selectionModel = new SingleSelectionModel<P>(keyProvider);
hasData.setSelectionModel(selectionModel);
selectionModel.addSelectionChangeHandler(new SelectionChangeEvent.Handler() {
public void onSelectionChange(SelectionChangeEvent event) {
P selectedObject = selectionModel.getSelectedObject();
if (selectedObject != null) {
showDetails(selectedObject);
}
}
});
}
public void createClicked() {
placeController.goTo(new ProxyPlace(proxyClass));
}
public ProxyListView<P> getView() {
return view;
}
public String mayStop() {
return null;
}
public void onCancel() {
onStop();
}
/**
* Called by the table as it needs data.
*/
public void onRangeChanged(HasData<P> listView) {
final Range range = listView.getVisibleRange();
final Receiver<List<P>> callback = new Receiver<List<P>>() {
@Override
public void onSuccess(List<P> values) {
if (view == null) {
// This activity is dead
return;
}
idToRow.clear();
idToProxy.clear();
for (int i = 0, row = range.getStart(); i < values.size(); i++, row++) {
P proxy = values.get(i);
@SuppressWarnings("unchecked")
// Why is this cast needed?
EntityProxyId<P> proxyId = (EntityProxyId<P>) proxy.stableId();
idToRow.put(proxyId, row);
idToProxy.put(proxyId, proxy);
}
getView().asHasData().setRowData(range.getStart(), values);
finishPendingSelection();
if (display != null) {
display.setWidget(getView());
}
}
};
fireRangeRequest(range, callback);
}
public void onStop() {
view.setDelegate(null);
view = null;
rangeChangeHandler.removeHandler();
rangeChangeHandler = null;
}
/**
* Select the given record, or clear the selection if called with null or an
* id we don't know.
*/
public void select(EntityProxyId<P> proxyId) {
/*
* The selectionModel will not flash if we put it back to the same state it
* is already in, so we can keep this code simple.
*/
// Clear the selection
P selected = selectionModel.getSelectedObject();
if (selected != null) {
selectionModel.setSelected(selected, false);
}
// Select the new proxy, if it's relevant
if (proxyId != null) {
P selectMe = idToProxy.get(proxyId);
if (selectMe != null) {
pendingSelection = null;
selectionModel.setSelected(selectMe, true);
} else {
/*
* It may be that an async request is about to fetch it.
* Make note to select it when it arrives (see
* finishPendingSelection()).
*/
pendingSelection = proxyId;
}
}
}
public void start(AcceptsOneWidget display, EventBus eventBus) {
view.setDelegate(this);
EntityProxyChange.registerForProxyType(eventBus, proxyClass,
new EntityProxyChange.Handler<P>() {
public void onProxyChange(EntityProxyChange<P> event) {
update(event.getWriteOperation(), event.getProxyId());
}
});
eventBus.addHandler(PlaceChangeEvent.TYPE, new PlaceChangeEvent.Handler() {
public void onPlaceChange(PlaceChangeEvent event) {
updateSelection(event.getNewPlace());
}
});
this.display = display;
init();
updateSelection(placeController.getWhere());
}
public void update(WriteOperation writeOperation, EntityProxyId<P> proxyId) {
switch (writeOperation) {
case UPDATE:
update(proxyId);
break;
case DELETE:
init();
break;
case PERSIST:
/*
* On create, we presume the new record is at the end of the list, so
* fetch the last page of items.
*/
getLastPage();
break;
}
}
protected abstract Request<List<P>> createRangeRequest(Range range);
protected abstract void fireCountRequest(Receiver<Long> callback);
/**
* Called when the user chooses a record to view. This default implementation
* sends the {@link PlaceController} to an appropriate {@link ProxyPlace}.
*
* @param record the chosen record
*/
protected void showDetails(P record) {
placeController.goTo(new ProxyPlace(record.stableId(), ProxyPlace.Operation.DETAILS));
}
@SuppressWarnings("unchecked")
private EntityProxyId<P> cast(ProxyPlace proxyPlace) {
return (EntityProxyId<P>) proxyPlace.getProxyId();
}
/**
* Finish selecting a proxy that hadn't yet arrived when
* {@link #select(EntityProxyId)} was called.
*/
private void finishPendingSelection() {
if (pendingSelection != null) {
P selectMe = idToProxy.get(pendingSelection);
pendingSelection = null;
if (selectMe != null) {
selectionModel.setSelected(selectMe, true);
}
}
}
private void fireRangeRequest(final Range range, final Receiver<List<P>> callback) {
createRangeRequest(range).with(getView().getPaths()).fire(callback);
}
private void getLastPage() {
fireCountRequest(new Receiver<Long>() {
@Override
public void onSuccess(Long response) {
if (view == null) {
// This activity is dead
return;
}
HasData<P> table = getView().asHasData();
int rows = response.intValue();
table.setRowCount(rows, true);
if (rows > 0) {
int pageSize = table.getVisibleRange().getLength();
int remnant = rows % pageSize;
if (remnant == 0) {
table.setVisibleRange(rows - pageSize, pageSize);
} else {
table.setVisibleRange(rows - remnant, pageSize);
}
}
onRangeChanged(table);
}
});
}
private void init() {
fireCountRequest(new Receiver<Long>() {
@Override
public void onSuccess(Long response) {
if (view == null) {
// This activity is dead
return;
}
getView().asHasData().setRowCount(response.intValue(), true);
onRangeChanged(view.asHasData());
}
});
}
private void update(EntityProxyId<P> proxyId) {
final Integer row = idToRow.get(proxyId);
if (row == null) {
return;
}
fireRangeRequest(new Range(row, 1), new Receiver<List<P>>() {
@Override
public void onSuccess(List<P> response) {
getView().asHasData().setRowData(row,
Collections.singletonList(response.get(0)));
}
});
}
private void updateSelection(Place newPlace) {
if (newPlace instanceof ProxyPlace) {
ProxyPlace proxyPlace = (ProxyPlace) newPlace;
if (proxyPlace.getOperation() != ProxyPlace.Operation.CREATE
&& proxyPlace.getProxyClass().equals(proxyClass)) {
select(cast(proxyPlace));
return;
}
}
select(null);
}
}