package kornell.gui.client.mvp; import java.util.LinkedHashSet; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; import com.github.gwtbootstrap.client.ui.constants.AlertType; import com.google.gwt.activity.shared.AbstractActivity; import com.google.gwt.activity.shared.Activity; import com.google.gwt.core.client.GWT; import com.google.gwt.event.shared.ResettableEventBus; import com.google.gwt.event.shared.UmbrellaException; import com.google.gwt.place.shared.PlaceChangeEvent; import com.google.gwt.place.shared.PlaceChangeRequestEvent; import com.google.gwt.user.client.Window; import com.google.gwt.user.client.ui.AcceptsOneWidget; import com.google.gwt.user.client.ui.IsWidget; import com.google.web.bindery.event.shared.EventBus; import com.google.web.bindery.event.shared.HandlerRegistration; import kornell.gui.client.util.view.KornellNotification; /** * Manages {@link Activity} objects that should be kicked off in response to * {@link PlaceChangeEvent} events. Each activity can start itself * asynchronously, and provides a widget to be shown when it's ready to run. */ public class AsyncActivityManager implements PlaceChangeEvent.Handler, PlaceChangeRequestEvent.Handler { Logger logger = Logger.getLogger(AsyncActivityManager.class.getName()); /** * Wraps our real display to prevent an Activity from taking it over if it * is not the currentActivity. */ private class ProtectedDisplay implements AcceptsOneWidget { private final Activity activity; ProtectedDisplay(Activity activity) { this.activity = activity; } public void setWidget(IsWidget view) { if (this.activity == AsyncActivityManager.this.currentActivity && view != null) { startingNext = false; showWidget(view); } } } private static final Activity NULL_ACTIVITY = new AbstractActivity() { @Override public void start(AcceptsOneWidget panel, com.google.gwt.event.shared.EventBus eventBus) { } }; private final AsyncActivityMapper mapper; private final EventBus eventBus; /* * Note that we use the legacy class from com.google.gwt.event.shared, * because we can't change the Activity interface. */ private final ResettableEventBus stopperedEventBus; private Activity currentActivity = NULL_ACTIVITY; private AcceptsOneWidget display; private boolean startingNext = false; private HandlerRegistration handlerRegistration; /** * Create an ActivityManager. Next call {@link #setDisplay}. * * @param mapper * finds the {@link Activity} for a given * {@link com.google.gwt.place.shared.Place} * @param eventBus * source of {@link PlaceChangeEvent} and * {@link PlaceChangeRequestEvent} events. */ public AsyncActivityManager(AsyncActivityMapper mapper, EventBus eventBus) { this.mapper = mapper; this.eventBus = eventBus; this.stopperedEventBus = new ResettableEventBus(eventBus); } /** * Returns an event bus which is in use by the currently running activity. * <p> * Any handlers attached to the returned event bus will be de-registered * when the current activity is stopped. * * @return the event bus used by the current activity */ public EventBus getActiveEventBus() { return stopperedEventBus; } /** * Deactivate the current activity, find the next one from our * ActivityMapper, and start it. * <p> * The current activity's widget will be hidden immediately, which can cause * flicker if the next activity provides its widget asynchronously. That can * be minimized by decent caching. Perenially slow activities might mitigate * this by providing a widget immediately, with some kind of "loading" * treatment. */ public void onPlaceChange(PlaceChangeEvent event) { getNextActivity(event,new ActivityCallbackHandler() { @Override public void onReceiveActivity(Activity nextActivity) { GWT.setUncaughtExceptionHandler(new GWT.UncaughtExceptionHandler() { @Override public void onUncaughtException(Throwable e) { e = unwrap(e); logger.log(Level.SEVERE, "Ex caught!", e); if(Window.Location.getHostName().indexOf("localhost") >= 0 || Window.Location.getHostName().indexOf("127.0.0.1") >= 0){ KornellNotification.show(e.getMessage(), AlertType.ERROR, 0); GWT.debugger(); } } public Throwable unwrap(Throwable e) { if(e instanceof UmbrellaException) { UmbrellaException ue = (UmbrellaException) e; if(ue.getCauses().size() == 1) { return unwrap(ue.getCauses().iterator().next()); } } return e; } }); Throwable caughtOnStop = null; Throwable caughtOnCancel = null; Throwable caughtOnStart = null; if (nextActivity == null) { nextActivity = NULL_ACTIVITY; } if (currentActivity.equals(nextActivity)) { return; } if (startingNext) { // The place changed again before the new current activity showed // its // widget caughtOnCancel = tryStopOrCancel(false); currentActivity = NULL_ACTIVITY; startingNext = false; } else if (!currentActivity.equals(NULL_ACTIVITY)) { showWidget(null); /* * Kill off the activity's handlers, so it doesn't have to worry * about them accidentally firing as a side effect of its tear down */ stopperedEventBus.removeHandlers(); caughtOnStop = tryStopOrCancel(true); } currentActivity = nextActivity; if (currentActivity.equals(NULL_ACTIVITY)) { showWidget(null); } else { startingNext = true; caughtOnStart = tryStart(); } if (caughtOnStart != null || caughtOnCancel != null || caughtOnStop != null) { Set<Throwable> causes = new LinkedHashSet<Throwable>(); if (caughtOnStop != null) { causes.add(caughtOnStop); } if (caughtOnCancel != null) { causes.add(caughtOnCancel); } if (caughtOnStart != null) { causes.add(caughtOnStart); } throw new UmbrellaException(causes); } } }); } /** * Reject the place change if the current activity is not willing to stop. * * @see com.google.gwt.place.shared.PlaceChangeRequestEvent.Handler#onPlaceChangeRequest(PlaceChangeRequestEvent) */ public void onPlaceChangeRequest(PlaceChangeRequestEvent event) { event.setWarning(currentActivity.mayStop()); } /** * Sets the display for the receiver, and has the side effect of starting or * stopping its monitoring the event bus for place change events. * <p> * If you are disposing of an ActivityManager, it is important to call * setDisplay(null) to get it to deregister from the event bus, so that it * can be garbage collected. * * @param display * an instance of AcceptsOneWidget */ public void setDisplay(AcceptsOneWidget display) { boolean wasActive = (null != this.display); boolean willBeActive = (null != display); this.display = display; if (wasActive != willBeActive) { updateHandlers(willBeActive); } } private void getNextActivity(PlaceChangeEvent event,ActivityCallbackHandler activityCallbackHandler) { if (display == null) { /* * Display may have been nulled during PlaceChangeEvent dispatch. * Don't bother the mapper, just return a null to ensure we shut * down the current activity */ return; } mapper.getActivity(event.getNewPlace(),activityCallbackHandler); } private void showWidget(IsWidget view) { if (display != null && view != null) { display.setWidget(view); } } private Throwable tryStart() { Throwable caughtOnStart = null; try { /* * Wrap the actual display with a per-call instance that protects * the display from canceled or stopped activities, and which * maintains our startingNext state. */ currentActivity.start(new ProtectedDisplay(currentActivity), stopperedEventBus); } catch (Throwable t) { caughtOnStart = t; } return caughtOnStart; } private Throwable tryStopOrCancel(boolean stop) { Throwable caughtOnStop = null; try { if (stop) { currentActivity.onStop(); } else { currentActivity.onCancel(); } } catch (Throwable t) { caughtOnStop = t; } finally { /* * Kill off the handlers again in case it was naughty and added new * ones during onstop or oncancel */ stopperedEventBus.removeHandlers(); } return caughtOnStop; } private void updateHandlers(boolean activate) { if (activate) { final HandlerRegistration placeReg = eventBus.addHandler( PlaceChangeEvent.TYPE, this); final HandlerRegistration placeRequestReg = eventBus.addHandler( PlaceChangeRequestEvent.TYPE, this); this.handlerRegistration = new HandlerRegistration() { public void removeHandler() { placeReg.removeHandler(); placeRequestReg.removeHandler(); } }; } else { if (handlerRegistration != null) { handlerRegistration.removeHandler(); handlerRegistration = null; } } } }