Added JavaDocs to RefreshManager and subclasses as well as ResultWithTTL

This commit is contained in:
Steffen Schaefer committed 2016-06-17 15:35:10 +02:00
1 parent 62365bf9cc
commit 340eb2a299
5 files changed
+108 -3

No files matched your search

@@ -8,6 +8,12 @@ import com.google.gwt.user.client.ui.AcceptsOneWidget;
import com.google.gwt.user.client.ui.Widget; import com.google.gwt.user.client.ui.Widget;
import com.sap.sailing.gwt.home.communication.SailingDispatchSystem; 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 { public class AutoAttachingRefreshManager extends LifecycleRefreshManager {
private static final Logger LOG = Logger.getLogger(AutoAttachingRefreshManager.class.getName()); private static final Logger LOG = Logger.getLogger(AutoAttachingRefreshManager.class.getName());
@@ -7,9 +7,23 @@ import com.google.gwt.event.logical.shared.AttachEvent.Handler;
import com.google.gwt.event.logical.shared.HasAttachHandlers; import com.google.gwt.event.logical.shared.HasAttachHandlers;
import com.sap.sailing.gwt.home.communication.SailingDispatchSystem; 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 { public class LifecycleRefreshManager extends RefreshManager {
private final HasAttachHandlers lifecycleWidget; 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) { public LifecycleRefreshManager(HasAttachHandlers lifecycleWidget, SailingDispatchSystem actionExecutor) {
super(actionExecutor); super(actionExecutor);
this.lifecycleWidget = lifecycleWidget; 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() { Scheduler.get().scheduleFinally(new ScheduledCommand() {
@Override @Override
public void execute() { public void execute() {
@@ -36,6 +53,8 @@ public class LifecycleRefreshManager extends RefreshManager {
@Override @Override
protected boolean canExecute() { 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(); return lifecycleWidget.isAttached();
} }
} }
@@ -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.DTO;
import com.sap.sse.gwt.dispatch.shared.commands.ResultWithTTL; 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 { public abstract class RefreshManager {
private static final Logger LOG = Logger.getLogger(RefreshManager.class.getName()); 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 static final long PAUSE_ON_ERROR = Duration.ONE_SECOND.times(30).asMillis();
private List<RefreshHolder<DTO, SailingAction<ResultWithTTL<DTO>>>> refreshables = new ArrayList<>(); private List<RefreshHolder<DTO, SailingAction<ResultWithTTL<DTO>>>> refreshables = new ArrayList<>();
@@ -34,30 +50,53 @@ public abstract class RefreshManager {
boolean started = false; boolean started = false;
/**
* @param actionExecutor the {@link SailingDispatchSystem} to use for remote communication
*/
public RefreshManager(SailingDispatchSystem actionExecutor) { public RefreshManager(SailingDispatchSystem actionExecutor) {
this.actionExecutor = 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() { protected void start() {
LOG.log(Level.FINE, "Starting auto refresh"); LOG.log(Level.FINE, "Starting auto refresh");
started = true; started = true;
reschedule(); 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() { protected void cancel() {
LOG.log(Level.FINE, "Cancelling auto refresh"); LOG.log(Level.FINE, "Cancelling auto refresh");
started = false; started = false;
timer.cancel(); timer.cancel();
} }
/**
* @return true if a refresh may occur in the current state, false otherwise.
*/
protected abstract boolean canExecute(); 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() { 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) { 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() { private void update() {
for (final RefreshHolder<DTO, SailingAction<ResultWithTTL<DTO>>> refreshable : refreshables) { for (final RefreshHolder<DTO, SailingAction<ResultWithTTL<DTO>>> refreshable : refreshables) {
@@ -94,6 +133,9 @@ public abstract class RefreshManager {
} }
} }
/**
* Ensures that the next update is scheduled by a timer.
*/
private void reschedule() { private void reschedule() {
if (scheduled) { if (scheduled) {
return; return;
@@ -129,6 +171,7 @@ public abstract class RefreshManager {
} }
} }
if(nextUpdate == null) { if(nextUpdate == null) {
// This can occur, if there is already a call running for all RefreshableWidgets
LOG.log(Level.FINE, "Nothing to auto update"); LOG.log(Level.FINE, "Nothing to auto update");
} else { } else {
int delayMillis = (int) (nextUpdate - System.currentTimeMillis()); int delayMillis = (int) (nextUpdate - System.currentTimeMillis());
@@ -151,6 +194,16 @@ public abstract class RefreshManager {
reschedule(); 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" }) @SuppressWarnings({ "rawtypes", "unchecked" })
public <D extends DTO, A extends SailingAction<ResultWithTTL<D>>> void add(RefreshableWidget<? super D> widget, public <D extends DTO, A extends SailingAction<ResultWithTTL<D>>> void add(RefreshableWidget<? super D> widget,
ActionProvider<A> provider) { ActionProvider<A> provider) {
@@ -158,6 +211,15 @@ public abstract class RefreshManager {
reschedule(); 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 <D extends DTO, A extends SailingAction<ResultWithTTL<D>>> void add(RefreshableWidget<? super D> widget, A action) { public <D extends DTO, A extends SailingAction<ResultWithTTL<D>>> void add(RefreshableWidget<? super D> widget, A action) {
add(widget, new DefaultActionProvider<>(action)); add(widget, new DefaultActionProvider<>(action));
} }
@@ -166,12 +228,24 @@ public abstract class RefreshManager {
return actionExecutor; return actionExecutor;
} }
/**
* Internal structure for holding the state for a {@link RefreshableWidget} controlled by the {@link RefreshManager}.
*
* @param <D> The Type of the Data returned from the server (wrapped by a {@link ResultWithTTL})
* @param <A> The type of the dispatch action for fetching new data
*/
private static class RefreshHolder<D extends DTO, A extends SailingAction<ResultWithTTL<D>>> { private static class RefreshHolder<D extends DTO, A extends SailingAction<ResultWithTTL<D>>> {
private final RefreshableWidget<D> widget; private final RefreshableWidget<D> widget;
private final ActionProvider<A> provider; private final ActionProvider<A> 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(); 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; private boolean callRunning = false;
public RefreshHolder(RefreshableWidget<D> widget, ActionProvider<A> provider) { public RefreshHolder(RefreshableWidget<D> widget, ActionProvider<A> provider) {
@@ -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.StringMessages;
import com.sap.sailing.gwt.ui.client.refresh.ErrorAndBusyClientFactory; 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 { public class RefreshManagerWithErrorAndBusy extends AutoAttachingRefreshManager {
private final AcceptsOneWidget container; private final AcceptsOneWidget container;
@@ -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. * Result implementation that holds a {@link DTO} with a defined time to live expiration.
* *
* @param <T> * @param <T> The type of the actual result that is wrapped by this.
*/ */
public class ResultWithTTL<T extends DTO> implements Result, HasClientCacheTotalTimeToLive { public class ResultWithTTL<T extends DTO> implements Result, HasClientCacheTotalTimeToLive {
/** max millis the result might be loaded earlier so we do batches **/ /** 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); 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 Duration ttl;
private T dto; private T dto;
@SuppressWarnings("unused") @SuppressWarnings("unused")