diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/refresh/AutoAttachingRefreshManager.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/refresh/AutoAttachingRefreshManager.java index 8392adea261..64d50bd6e39 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/refresh/AutoAttachingRefreshManager.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/refresh/AutoAttachingRefreshManager.java @@ -8,6 +8,12 @@ import com.google.gwt.user.client.ui.AcceptsOneWidget; import com.google.gwt.user.client.ui.Widget; import com.sap.sailing.gwt.home.communication.SailingDispatchSystem; +/** + * Specialized {@link LifecycleRefreshManager} that is bound to the lifecycle of a given {@link AcceptsOneWidget} and + * ensures that a content {@link Widget} is being attached when the first refresh is successfully finished. This enures + * that no uninitialized {@link RefreshableWidget} are initially shown. Instead you can explicitly set a placeholder + * widget to the container that is automatically replaced with the actual content when the data is available. + */ public class AutoAttachingRefreshManager extends LifecycleRefreshManager { private static final Logger LOG = Logger.getLogger(AutoAttachingRefreshManager.class.getName()); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/refresh/LifecycleRefreshManager.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/refresh/LifecycleRefreshManager.java index 6ae8c579121..addce5d29bb 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/refresh/LifecycleRefreshManager.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/refresh/LifecycleRefreshManager.java @@ -7,9 +7,23 @@ import com.google.gwt.event.logical.shared.AttachEvent.Handler; import com.google.gwt.event.logical.shared.HasAttachHandlers; import com.sap.sailing.gwt.home.communication.SailingDispatchSystem; +/** + * Specialized {@link RefreshManager} that is being bound to the lifecycle of a given widget. This is typically a + * container that holds the layout of the inner refreshable parts. + * + * This should always be used when refresh functionality is used by a UI part that can disappear sometime. This is e.g + * the case for Activities that are replaced by another Activity when a new Place fires. + * + * The implementation ensures that no memory leak is produced by the inner Timer instance by cleanly stopping all work + * when the lifecycle widget is detached from the DOM tree. + */ public class LifecycleRefreshManager extends RefreshManager { private final HasAttachHandlers lifecycleWidget; + /** + * @param lifecycleWidget the widget that defines the lifecycle of this {@link RefreshManager}. + * @param actionExecutor the {@link SailingDispatchSystem} to use for remote communication + */ public LifecycleRefreshManager(HasAttachHandlers lifecycleWidget, SailingDispatchSystem actionExecutor) { super(actionExecutor); this.lifecycleWidget = lifecycleWidget; @@ -24,6 +38,9 @@ public class LifecycleRefreshManager extends RefreshManager { } }); + // if the widget is already attached, the listener above won't fire, so we trigger the first refresh directly. + // scheduleFinally is used to wait until all RefreshableWidgets are added. + // Otherwise, there would be no RefreshableWidget and the initial refresh would be skipped, so that not refresh would occur at all. Scheduler.get().scheduleFinally(new ScheduledCommand() { @Override public void execute() { @@ -36,6 +53,8 @@ public class LifecycleRefreshManager extends RefreshManager { @Override protected boolean canExecute() { + // Refreshes can only occur if the lifecycleWidget is actually attached to the DOM tree. + // Other wise we would produce a memory consuming zombie that eats the browser's resources and unnecessarily fetches data from the server. return lifecycleWidget.isAttached(); } } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/refresh/RefreshManager.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/refresh/RefreshManager.java index 2f22649284b..7853b5a796c 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/refresh/RefreshManager.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/refresh/RefreshManager.java @@ -16,9 +16,25 @@ import com.sap.sse.common.Duration; import com.sap.sse.gwt.dispatch.shared.commands.DTO; import com.sap.sse.gwt.dispatch.shared.commands.ResultWithTTL; +/** + * Provides automatic refresh functionality for widgets based on the {@link SailingDispatchSystem}. + * + * A widget can be registered for automatic refresh if it implements {@link RefreshableWidget} and it's data is provided + * by a dispatch action of type {@link SailingAction}. This action needs to have it's result wrapped by a + * {@link ResultWithTTL} to control refresh intervals by the server. + * + * The {@link RefreshManager} ensures that the next refresh will occur in about the time defined by the returned + * {@link ResultWithTTL}. While doing this it tries to batch refreshes that are scheduled for a similar time point. + * + * If an error occurs while loading new data on a refresh, the next refresh is automatically scheduled in 30 seconds to + * not kill the server by immediate repetitive remote calls. + */ public abstract class RefreshManager { private static final Logger LOG = Logger.getLogger(RefreshManager.class.getName()); + /** + * When an error occurs, the next refresh is delayed by 30 seconds to not kill the server by doing immediate repetitive calls. + */ private static final long PAUSE_ON_ERROR = Duration.ONE_SECOND.times(30).asMillis(); private List>>> refreshables = new ArrayList<>(); @@ -34,30 +50,53 @@ public abstract class RefreshManager { boolean started = false; + /** + * @param actionExecutor the {@link SailingDispatchSystem} to use for remote communication + */ public RefreshManager(SailingDispatchSystem actionExecutor) { this.actionExecutor = actionExecutor; } + /** + * Initially starts the auto-refresh. Needs to be called once by sublasses when at least one {@link RefreshableWidget} is added. + * This triggers the first refresh. Subsequent refreshes are automatically triggered until {@link #cancel()} is called. + */ protected void start() { LOG.log(Level.FINE, "Starting auto refresh"); started = true; reschedule(); } + /** + * Must be called by subclasses to stop the auto-refresh. Ensures that all resources are disposed that would + * otherwise lead to memory leaks or zombie processes. + */ protected void cancel() { LOG.log(Level.FINE, "Cancelling auto refresh"); started = false; timer.cancel(); } + /** + * @return true if a refresh may occur in the current state, false otherwise. + */ protected abstract boolean canExecute(); + /** + * Is called whenever a refresh was successful. Useful for subclasses that need to hook into the lifecycle of the {@link RefreshManager}. + */ protected void onSuccessfulUpdate() { } + /** + * Is called whenever a refresh failed. Useful for subclasses that need to hook into the lifecycle of the {@link RefreshManager}. + */ protected void onFailedUpdate(Throwable errorCause) { } + /** + * Triggers an update for all {@link RefreshableWidget}s that will have a timeout in the next 5 seconds. + */ private void update() { for (final RefreshHolder>> refreshable : refreshables) { @@ -94,6 +133,9 @@ public abstract class RefreshManager { } } + /** + * Ensures that the next update is scheduled by a timer. + */ private void reschedule() { if (scheduled) { return; @@ -129,6 +171,7 @@ public abstract class RefreshManager { } } if(nextUpdate == null) { + // This can occur, if there is already a call running for all RefreshableWidgets LOG.log(Level.FINE, "Nothing to auto update"); } else { int delayMillis = (int) (nextUpdate - System.currentTimeMillis()); @@ -151,6 +194,16 @@ public abstract class RefreshManager { reschedule(); } + /** + * Add a {@link RefreshableWidget} to the set of refreshables controlled by this {@link RefreshManager}. + * The refresh intervals are controlled by the ResultWithTTL returned from the server. + * + * On each refresh, the given {@link ActionProvider} is called to get the action to send to the server. + * This is useful if some state-dependent data needs to be sent so that the action can't be a fix instance. + * + * @param widget the {@link RefreshableWidget} to start auto refresh for + * @param provider the {@link ActionProvider} to get the {@link SailingAction} instance from when fetching new data for a refresh + */ @SuppressWarnings({ "rawtypes", "unchecked" }) public >> void add(RefreshableWidget widget, ActionProvider provider) { @@ -158,6 +211,15 @@ public abstract class RefreshManager { reschedule(); } + /** + * Add a {@link RefreshableWidget} to the set of refreshables controlled by this {@link RefreshManager}. + * The refresh intervals are controlled by the ResultWithTTL returned from the server. + * + * On each refresh, the given action is sent to the server to get new data. + * + * @param widget the {@link RefreshableWidget} to start auto refresh for + * @param action the {@link SailingAction} instance to use when fetching new data for a refresh + */ public >> void add(RefreshableWidget widget, A action) { add(widget, new DefaultActionProvider<>(action)); } @@ -166,12 +228,24 @@ public abstract class RefreshManager { return actionExecutor; } + /** + * Internal structure for holding the state for a {@link RefreshableWidget} controlled by the {@link RefreshManager}. + * + * @param The Type of the Data returned from the server (wrapped by a {@link ResultWithTTL}) + * @param The type of the dispatch action for fetching new data + */ private static class RefreshHolder>> { private final RefreshableWidget widget; private final ActionProvider provider; - // initial update is now + /** + * Defines the timeout when the next refresh will occur. + * Initial update is now to trigger an immediate update. + */ private long timeout = System.currentTimeMillis(); + /** + * When there is currently a call running for refreshing this {@link RefreshableWidget}, callRunning is set to true. + */ private boolean callRunning = false; public RefreshHolder(RefreshableWidget widget, ActionProvider provider) { diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/refresh/RefreshManagerWithErrorAndBusy.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/refresh/RefreshManagerWithErrorAndBusy.java index c22b5967e9c..be706d7d178 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/refresh/RefreshManagerWithErrorAndBusy.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/refresh/RefreshManagerWithErrorAndBusy.java @@ -6,6 +6,12 @@ import com.sap.sailing.gwt.home.communication.SailingDispatchSystem; import com.sap.sailing.gwt.ui.client.StringMessages; import com.sap.sailing.gwt.ui.client.refresh.ErrorAndBusyClientFactory; +/** + * Specialized {@link AutoAttachingRefreshManager} that initially shows a busy widget that is replayced with the actual + * content when the first refresh is finished. In the case of an error while doing the first refresh, an error widget is + * shown. When an error occurs on a further refresh, the old content simply isn't exchanged an a refresh is triggered as + * described for {@link RefreshManager}. + */ public class RefreshManagerWithErrorAndBusy extends AutoAttachingRefreshManager { private final AcceptsOneWidget container; diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/dispatch/shared/commands/ResultWithTTL.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/dispatch/shared/commands/ResultWithTTL.java index b87687d1ae2..a3101baa2f9 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/dispatch/shared/commands/ResultWithTTL.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/dispatch/shared/commands/ResultWithTTL.java @@ -7,14 +7,14 @@ import com.sap.sse.gwt.dispatch.shared.caching.HasClientCacheTotalTimeToLive; /** * Result implementation that holds a {@link DTO} with a defined time to live expiration. * - * @param + * @param The type of the actual result that is wrapped by this. */ public class ResultWithTTL implements Result, HasClientCacheTotalTimeToLive { /** max millis the result might be loaded earlier so we do batches **/ public static final Duration MAX_TIME_TO_LOAD_EARLIER = Duration.ONE_SECOND.times(5); - /** Time to live*/ + /** Time to live. Defines when the client should ask for data the next time.*/ private Duration ttl; private T dto; @SuppressWarnings("unused")