/*
* AbstractRefreshManager.java
*
* Copyright (C) 2010 Leo Osvald <leo.osvald@gmail.com>
*
* This file is part of SGLJ.
*
* SGLJ is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* SGLJ is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this library. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sglj.sync;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;
/**
* Abstract implementation of {@link RefreshManager} interface.<br>
* All implemented methods are thread-safe.
*
* <br>
* Note: To avoid nondeterministic behavior caused by multiple threads,
* it is mandatory that all {@link Refreshable} implementations have
* their methods {@link #equals(Object)} and {@link #hashCode()}
* implemented in a thread-safe manner.
*
* @author Leo Osvald
* @version 0.91
*/
public abstract class AbstractRefreshManager implements RefreshManager,
RefreshListener {
/**
* Map which contains all refreshables and their timers which
* are responsible for automatic refresh.<br>
* Refreshables that are not being automatically refreshed have
* <code>null</code> value associated in this map.<br>
* This map is thread-safe.<br>
* <b>Note</b>: For iterating through this map, external synchronization
* is required.
*/
protected final Map<Refreshable, RefreshTimerTask> timerTasks;
/**
* Timer which is responsible for scheduling refresh tasks.
*/
protected final RefreshTimer timer;
/**
* Mutex which serves for synchronization purposes regarding
* scheduling for refresh.<br>
* The following methods should generally be called from a
* synchronized block (on this mutex):
* <ul>
* <li>{@link #scheduleForAutoRefresh(Refreshable)}</li>
* <li>{@link #initAutoRefresh()}</li>
* <li>{@link #decideToProceed(RefreshTimerTask)}</li>
* <li>{@link #refreshRejected(RefreshTimerTask)}</li>
* </ul>
* </pre>
*/
protected final Object scheduleMutex = new Object();
private volatile boolean enabled;
private final Set<Refreshable> autoRefreshSet;
public AbstractRefreshManager() {
timer = new RefreshTimer();
timerTasks = Collections.synchronizedMap(
new HashMap<Refreshable, RefreshTimerTask>());
autoRefreshSet = new HashSet<Refreshable>();
}
@Override
public void addRefreshable(Refreshable refreshable, boolean autoRefresh) {
if(!timerTasks.containsKey(refreshable)) {
setAutoRefreshEnabled(refreshable, autoRefresh);
}
}
@Override
public void setAutoRefreshEnabled(Refreshable refreshable,
boolean autoRefresh) {
boolean changed = true;
synchronized (timerTasks) {
//if this refreshable exists and hasn't changed its state
//we have nothing to change
if(timerTasks.containsKey(refreshable)
&& (timerTasks.get(refreshable) != null) == autoRefresh)
changed = false;
}
if(changed) {
if(autoRefresh) {
refreshable.addRefreshListener(this);
if(isAutoRefreshEnabled()) {
synchronized (scheduleMutex) {
timerTasks.put(refreshable, scheduleForAutoRefresh(refreshable));
}
}
synchronized (autoRefreshSet) {
autoRefreshSet.add(refreshable);
}
}
else {
TimerTask timerTask = timerTasks.get(refreshable);
timerTasks.put(refreshable, null);
refreshable.removeRefreshListener(this);
if(isAutoRefreshEnabled()) {
if(timerTask != null) {
timerTask.cancel();
}
}
synchronized (autoRefreshSet) {
autoRefreshSet.remove(refreshable);
}
}
}
}
@Override
public boolean isAutoRefreshEnabled(Refreshable refreshable) {
return timerTasks.get(refreshable) != null;
}
@Override
public boolean removeRefreshable(Refreshable refreshable) {
RefreshTimerTask timerTask = timerTasks.get(refreshable);
if(timerTask != null) {
timerTasks.remove(timerTask.refreshable);
timerTask.cancel();
if(isAutoRefreshEnabled(timerTask.refreshable))
timerTask.refreshable.removeRefreshListener(this);
return true;
}
synchronized (autoRefreshSet) {
autoRefreshSet.remove(refreshable);
}
return false;
}
@Override
public void setAutoRefreshEnabled(boolean enabled) {
if(this.enabled != enabled) {
this.enabled = enabled;
initAutoRefresh();
}
}
@Override
public boolean isAutoRefreshEnabled() {
return enabled;
}
@Override
public Set<Refreshable> getRefreshables() {
Set<Refreshable> ret = new HashSet<Refreshable>();
synchronized (timerTasks) {
ret.addAll(timerTasks.keySet());
return ret;
}
}
/**
* This method is called when refresh task is run from inside
* the {@link RefreshTimerTask#run()} method.<br>
* This is the right place for eventual decision whether refresh task
* should proceed with refresh or not.
* @param task refresh task
* @return <code>true</code> if refresh task should proceed,
* <code>false</code> otherwise.
*/
protected abstract boolean decideToProceed(RefreshTimerTask task);
/**
* This method is called when refresh task is rejected
* (i.e. the same refreshable has just been refreshed).
* @param task
*/
protected abstract void refreshRejected(RefreshTimerTask task);
/**
* This method should do the scheduling of the specified refreshable.
* @param refreshable refreshable
* @return refresh task which was scheduled or <code>null</code> if no
* refresh task was scheduled.
*/
protected abstract RefreshTimerTask scheduleForAutoRefresh(Refreshable refreshable);
/**
* Initializes autorefresh.<br>
* This method is called automatically whenever automatic refresh
* is enabled or disabled (if state actually changed).
*/
protected void initAutoRefresh() {
synchronized (scheduleMutex) {
Object[] refs;
synchronized (autoRefreshSet) {
//copy tasks to avoid ConcurrentModificationException
//in scheduleForAutoRefresh
refs = autoRefreshSet.toArray();
}
if(enabled) {
//if this timer is unable to schedule new tasks, create a new one
// Object[] tasks;
// synchronized (timerTasks) { //must lock here
// //copy tasks to avoid ConcurrentModificationException
// //in scheduleForAutoRefresh
// tasks = timerTasks.values().toArray();
// }
// for(Object task : tasks)
// if(task != null) {
// Refreshable refreshable = ((RefreshTimerTask)task).refreshable();
// timerTasks.put(refreshable, scheduleForAutoRefresh(refreshable));
// if(Environment.isDevelopment())
// System.out.println("Enabling: "+refreshable);
// }
for(Object o : refs) {
Refreshable refreshable = (Refreshable) o;
timerTasks.put(refreshable, scheduleForAutoRefresh(refreshable));
// System.out.println("Enabling: "+refreshable);
}
}
else {
for(Object o : refs) {
Refreshable refreshable = (Refreshable) o;
RefreshTimerTask task = timerTasks.get(refreshable);
// System.out.println("Disabling: "+refreshable);
if(task != null)
task.cancel();
}
}
}
}
/**
* <p>Refresh task.<br>
* Its {@link #run()} method behaves like this:
* First,
* {@link AbstractRefreshManager#decideToProceed(RefreshTimerTask)}
* method is called. If call returns <code>true</code>,
* {@link Refreshable#refresh()} method is called on corresponding
* refreshable component. Otherwise,
* {@link AbstractRefreshManager#refreshRejected(RefreshTimerTask)}
* method is called.</p>
* <p>Methods {@link AbstractRefreshManager#decideToProceed(RefreshTimerTask)}
* and {@link AbstractRefreshManager#refreshRejected(RefreshTimerTask)}
* are synchronized on the same object, so that decision whether some
* future task should proceed with refresh can depend on the one currently
* rejected.
*
* @author Leo Osvald
*
*/
protected class RefreshTimerTask extends TimerTask {
private final Refreshable refreshable;
public RefreshTimerTask(Refreshable refreshable) {
this.refreshable = refreshable;
}
@Override
public void run() {
try {
// System.out.println("Started running task: " +refreshable);
boolean proceed;
synchronized (scheduleMutex) {
proceed = decideToProceed(this);
}
if(proceed) {
refreshable.refresh();
}
else {
synchronized (scheduleMutex) {
refreshRejected(this);
}
}
// System.out.println("Ended running task: "+refreshable);
} catch(Exception ex) {
ex.printStackTrace();
}
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + getOuterType().hashCode();
result = prime * result
+ ((refreshable == null) ? 0 : refreshable.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (!(obj instanceof RefreshTimerTask))
return false;
RefreshTimerTask other = (RefreshTimerTask) obj;
if (!getOuterType().equals(other.getOuterType()))
return false;
if (refreshable == null) {
if (other.refreshable != null)
return false;
} else if (!refreshable.equals(other.refreshable))
return false;
return true;
}
public Refreshable refreshable() {
return refreshable;
}
private AbstractRefreshManager getOuterType() {
return AbstractRefreshManager.this;
}
}
protected static class RefreshTimer extends Timer {
public RefreshTimer() {
super(true);
}
/**
* Does nothing.
*/
@Override
public void cancel() {
}
}
// /**
// * This method is called automatically from
// * {@link RefreshTimerTask#run()} method, immediately before
// * call to refresh method on the task if manager decided to proceed with
// * its refresh.
// * @param task refresh task
// */
// protected abstract void autoRefreshStarted(RefreshTimerTask task);
//
// /**
// * This method is called right after {@link Refreshable#refresh()}
// * methods ends.
// * @param task
// */
// protected abstract void autoRefreshDone(RefreshTimerTask task);
}