package org.activityinfo.ui.client.local.sync;
/*
* #%L
* ActivityInfo Server
* %%
* Copyright (C) 2009 - 2013 UNICEF
* %%
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public
* License along with this program. If not, see
* <http://www.gnu.org/licenses/gpl-3.0.html>.
* #L%
*/
import com.bedatadriven.rebar.async.AsyncCommand;
import com.bedatadriven.rebar.sql.client.SqlDatabase;
import com.bedatadriven.rebar.sql.client.SqlException;
import com.bedatadriven.rebar.sql.client.SqlTransactionCallback;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import org.activityinfo.i18n.shared.UiConstants;
import org.activityinfo.legacy.client.Dispatcher;
import org.activityinfo.legacy.client.remote.Remote;
import org.activityinfo.legacy.shared.Log;
import org.activityinfo.legacy.shared.command.GetSyncRegionUpdates;
import org.activityinfo.legacy.shared.command.GetSyncRegions;
import org.activityinfo.legacy.shared.command.result.SyncRegion;
import org.activityinfo.legacy.shared.command.result.SyncRegionUpdate;
import org.activityinfo.legacy.shared.command.result.SyncRegions;
import org.activityinfo.ui.client.EventBus;
import org.activityinfo.ui.client.local.command.CommandQueue;
import java.util.Collection;
import java.util.Date;
import java.util.Iterator;
/**
* Synchronizes the local database by retriving updates from the remote server.
*/
@Singleton
public class DownSynchronizer implements AsyncCommand {
private final Dispatcher dispatch;
private final EventBus eventBus;
private final SqlDatabase conn;
private final UiConstants uiConstants;
private ProgressTrackingIterator<SyncRegion> regionIt;
private AsyncCallback<Void> callback;
private boolean running = false;
private SyncRegionTable localVerisonTable;
private SyncHistoryTable historyTable;
private CommandQueue commandQueueTable;
private SynchronizerStats stats = new SynchronizerStats();
@Inject
public DownSynchronizer(EventBus eventBus, @Remote Dispatcher dispatch, SqlDatabase conn, UiConstants uiConstants) {
this.eventBus = eventBus;
this.conn = conn;
this.dispatch = dispatch;
this.uiConstants = uiConstants;
this.localVerisonTable = new SyncRegionTable(conn);
this.historyTable = new SyncHistoryTable(conn);
this.commandQueueTable = new CommandQueue(eventBus, conn);
}
@Override
public void execute(AsyncCallback<Void> callback) {
this.callback = callback;
fireStatusEvent(uiConstants.requestingSyncRegions(), 0);
running = true;
stats.onStart();
retrieveSyncRegions();
}
@SuppressWarnings({"unchecked", "rawtypes"})
private void retrieveSyncRegions() {
dispatch.execute(new GetSyncRegions(), new AsyncCallback<SyncRegions>() {
@Override
public void onFailure(Throwable throwable) {
handleException("Error getting sync regions", throwable);
}
@Override
public void onSuccess(SyncRegions syncRegions) {
DownSynchronizer.this.regionIt = new ProgressTrackingIterator(syncRegions.getList());
fireStatusEvent("Received sync regions...", 0);
doNextUpdate();
}
});
}
private void fireStatusEvent(String task, double percentComplete) {
Log.info("Synchronizer: " + task + " (" + percentComplete + "%)");
eventBus.fireEvent(SyncStatusEvent.TYPE, new SyncStatusEvent(task, percentComplete));
}
private void doNextUpdate() {
if (!running) {
return;
}
if (regionIt.hasNext()) {
final SyncRegion region = regionIt.next();
updateLocalVersion(region);
} else {
onSynchronizationComplete();
}
}
private void updateLocalVersion(final SyncRegion region) {
localVerisonTable.get(region.getId(), new DefaultCallback<String>() {
@Override
public void onSuccess(String localVersion) {
if (localVersion == null ||
region.getCurrentVersion() == null ||
!localVersion.equals(region.getCurrentVersion())) {
doUpdate(region, localVersion);
} else {
Log.debug("Region " + region.getId() + " is up to date");
doNextUpdate();
}
}
});
}
private void onSynchronizationComplete() {
stats.onFinished();
setLastUpdateTime();
fireStatusEvent(uiConstants.synchronizationComplete(), 100);
eventBus.fireEvent(new SyncCompleteEvent(new Date()));
if (callback != null) {
callback.onSuccess(null);
}
}
private void doUpdate(final SyncRegion region, String localVersion) {
fireStatusEvent(uiConstants.downSyncProgress(), regionIt.percentComplete());
Log.info("Synchronizer: Region " + region.getId() + ": localVersion=" + localVersion);
stats.onRemoteCallStarted();
dispatch.execute(new GetSyncRegionUpdates(region.getId(), localVersion), new AsyncCallback<SyncRegionUpdate>() {
@Override
public void onFailure(Throwable throwable) {
handleException("GetSyncRegionUpdates for region id " + region.getId() + " failed.", throwable);
}
@Override
public void onSuccess(SyncRegionUpdate update) {
stats.onRemoteCallFinished();
persistUpdates(region, update);
}
});
}
private void persistUpdates(final SyncRegion region, final SyncRegionUpdate update) {
if (update.getSql() == null) {
Log.debug("Synchronizer: Region " + region.getId() + " is up to date");
doNextUpdate();
} else {
Log.debug("Synchronizer: persisting updates for region " + region.getId());
stats.onDbUpdateStarted();
conn.executeUpdates(update.getSql(), new AsyncCallback<Integer>() {
@Override
public void onFailure(Throwable throwable) {
handleException("Synchronizer: Async execution of region " + region.getId() + " failed." +
"\nMessage: " + throwable.getMessage(), throwable);
}
@Override
public void onSuccess(Integer rows) {
Log.debug("Synchronizer: updates to region " + region.getId() + " succeeded, " + rows +
" row(s) affected");
stats.onDbUpdateFinished();
updateLocalVersion(region, update);
}
});
}
}
private void updateLocalVersion(final SyncRegion region, final SyncRegionUpdate update) {
localVerisonTable.put(region.getId(), update.getVersion(), new AsyncCallback<Void>() {
@Override
public void onSuccess(Void result) {
if (!update.isComplete()) {
doUpdate(region, update.getVersion());
} else {
doNextUpdate();
}
}
@Override
public void onFailure(Throwable caught) {
callback.onFailure(caught);
}
});
}
private void handleException(String message, Throwable throwable) {
Log.error("Synchronizer: " + message, throwable);
if (callback != null) {
callback.onFailure(throwable);
}
}
private void setLastUpdateTime() {
historyTable.update();
}
private abstract class DefaultTxCallback extends SqlTransactionCallback {
@Override
public final void onError(SqlException e) {
callback.onFailure(e);
}
}
private abstract class DefaultCallback<T> implements AsyncCallback<T> {
@Override
public void onFailure(Throwable caught) {
callback.onFailure(caught);
}
}
private static final class ProgressTrackingIterator<T> implements Iterator<T> {
private double total;
private double completed;
private Iterator<T> delegateIterator;
private ProgressTrackingIterator(Collection<T> collection) {
total = collection.size();
completed = 0;
delegateIterator = collection.iterator();
}
@Override
public boolean hasNext() {
completed++;
return delegateIterator.hasNext();
}
@Override
public T next() {
return delegateIterator.next();
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
public double percentComplete() {
return completed / total * 100d;
}
}
public void getLastUpdateTime(AsyncCallback<Date> callback) {
historyTable.get(callback);
}
}