package org.sigmah.shared.file;
/*
* #%L
* Sigmah
* %%
* Copyright (C) 2010 - 2016 URD
* %%
* 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 java.util.ArrayList;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.sigmah.client.dispatch.DispatchAsync;
import org.sigmah.client.i18n.I18N;
import org.sigmah.client.ui.notif.N10N;
import org.sigmah.client.ui.widget.form.ButtonFileUploadField;
import org.sigmah.offline.dao.FileDataAsyncDAO;
import org.sigmah.offline.dao.TransfertAsyncDAO;
import org.sigmah.offline.fileapi.ArrayBuffer;
import org.sigmah.offline.fileapi.Blob;
import org.sigmah.offline.fileapi.FileReader;
import org.sigmah.offline.fileapi.Int8Array;
import org.sigmah.offline.fileapi.LoadFileAdapter;
import org.sigmah.offline.js.FileDataJS;
import org.sigmah.offline.js.FileVersionJS;
import org.sigmah.offline.js.TransfertJS;
import org.sigmah.offline.js.Values;
import org.sigmah.offline.status.ApplicationState;
import org.sigmah.shared.command.PrepareFileUpload;
import org.sigmah.shared.dto.value.FileVersionDTO;
import org.sigmah.shared.util.FileType;
import com.allen_sauer.gwt.log.client.Log;
import com.extjs.gxt.ui.client.widget.form.Field;
import com.extjs.gxt.ui.client.widget.form.FormPanel;
import com.google.gwt.core.client.JsArray;
import com.google.gwt.user.client.Timer;
import com.google.gwt.user.client.rpc.AsyncCallback;
import org.sigmah.client.event.EventBus;
import org.sigmah.client.event.OfflineEvent;
import org.sigmah.client.event.handler.OfflineHandler;
import org.sigmah.offline.fileapi.Files;
/**
* Transfert files by slicing them.
* Files are stored inside IndexedDB to allow offline re-download and on-connect
* upload.
*
* When uploading, files are stored before the transfert.
*
* When downloading, files are transfered, then stored inside IndexedDB and
* finally actually downloaded by the client.
*
* @author Raphaƫl Calabro (rcalabro@ideia.fr)
*/
class Html5TransfertManager implements TransfertManager, HasProgressListeners {
/**
* Size in bytes of the first slice.
*/
static final int BASE_SLICE_SIZE = 100 * 1024;
/**
* Time in seconds to send or receive one slice.
*/
static final double SLICE_TRANSFERT_TIME = 2.0;
/**
* Maximum number of retries in case of download or upload failure.
*/
private static final int MAXIMUM_NUMBER_OF_RETRIES = 5;
private final EventBus eventBus;
private final List<Task> downloads;
private final List<Task> uploads;
private final int[] currentTasks;
private final TransfertThread downloadThread;
private final TransfertThread uploadThread;
private final DispatchAsync dispatchAsync;
private final FileDataAsyncDAO fileDataAsyncDAO;
private final TransfertAsyncDAO transfertAsyncDAO;
private final DirectTransfertManager directTransfertManager;
private ApplicationState state;
private final Map<TransfertType, ProgressListener> progressListeners;
public Html5TransfertManager(DispatchAsync dispatchAsync, FileDataAsyncDAO fileDataAsyncDAO, TransfertAsyncDAO transfertAsyncDAO, EventBus eventBus, DirectTransfertManager directTransfertManager) {
this.dispatchAsync = dispatchAsync;
this.fileDataAsyncDAO = fileDataAsyncDAO;
this.transfertAsyncDAO = transfertAsyncDAO;
this.eventBus = eventBus;
this.directTransfertManager = directTransfertManager;
// Creating transfert threads.
this.downloads = new ArrayList<Task>();
this.uploads = new ArrayList<Task>();
this.currentTasks = new int[]{-1, -1};
downloadThread = createTransfertThread();
uploadThread = createTransfertThread();
this.progressListeners = new EnumMap<TransfertType, ProgressListener>(TransfertType.class);
// Adding a connection status listener to start or stop threads
listenToConnectionStatusChanges(eventBus);
}
// Transfert thread handling -----------------------------------------------
private TransfertThread createTransfertThread() {
final TransfertThread transfertThread = new TransfertThread();
transfertThread.setTransfertManager(this);
transfertThread.setDispatcher(dispatchAsync);
transfertThread.setTransfertAsyncDAO(transfertAsyncDAO);
return transfertThread;
}
private void listenToConnectionStatusChanges(EventBus eventBus) {
eventBus.addHandler(OfflineEvent.getType(), new OfflineHandler() {
@Override
public void handleEvent(OfflineEvent event) {
state = event.getState();
downloadThread.setOnline(state == ApplicationState.ONLINE);
uploadThread.setOnline(state == ApplicationState.ONLINE);
}
});
}
public void queueTransfert(TransfertJS transfertJS, ProgressListener listener) {
queueTask(new Task(transfertJS, listener));
}
public void queueTask(Task task) {
switch(task.getTransfert().getType()) {
case DOWNLOAD:
downloads.add(task);
nextDownload();
break;
case UPLOAD:
uploads.add(task);
nextUpload();
break;
}
}
public void nextDownload() {
nextTask(TransfertType.DOWNLOAD, downloadThread, downloads);
}
public void nextUpload() {
nextTask(TransfertType.UPLOAD, uploadThread, uploads);
}
private void nextTask(TransfertType type, TransfertThread thread, List<Task> tasks) {
if(thread.isAvailable()) {
currentTasks[type.ordinal()]++;
if(currentTasks[type.ordinal()] < tasks.size()) {
final Task task = tasks.get(currentTasks[type.ordinal()]);
thread.setTask(task);
} else if(currentTasks[type.ordinal()] == tasks.size()) {
currentTasks[type.ordinal()] = -1;
tasks.clear();
}
}
}
public void onTransfertFailure(final Task task, TransfertThread transfertThread) {
if(task.getTries() < MAXIMUM_NUMBER_OF_RETRIES) {
task.setTries(task.getTries() + 1);
final Timer timer = new Timer() {
@Override
public void run() {
queueTask(task);
}
};
timer.schedule(3000 * task.getTries());
}
switch(task.getTransfert().getType()) {
case DOWNLOAD:
nextDownload();
break;
case UPLOAD:
nextUpload();
break;
}
}
// Downloads ---------------------------------------------------------------
/**
* {@inheritDoc}
*/
@Override
public void download(final FileVersionDTO fileVersionDTO, final ProgressListener progressListener) {
fileDataAsyncDAO.getByFileVersionId(fileVersionDTO.getId(), new AsyncCallback<FileDataJS>() {
@Override
public void onFailure(Throwable caught) {
progressListener.onFailure(Cause.CACHE_ERROR);
}
@Override
public void onSuccess(FileDataJS fileDataJS) {
if(fileDataJS == null) {
queueTransfert(TransfertJS.createTransfertJS(fileVersionDTO, TransfertType.DOWNLOAD), progressListener);
} else {
startDownload(fileVersionDTO, fileDataJS.getData());
}
}
});
}
/**
* {@inheritDoc}
*/
@Override
public void cache(final FileVersionDTO fileVersionDTO) {
fileDataAsyncDAO.getByFileVersionId(fileVersionDTO.getId(), new AsyncCallback<FileDataJS>() {
@Override
public void onFailure(Throwable caught) {
Log.error("Error while saving locally the file '" + fileVersionDTO.getName() + "'.", caught);
}
@Override
public void onSuccess(FileDataJS result) {
if(result == null) {
queueTask(new Task(TransfertJS.createTransfertJS(fileVersionDTO, TransfertType.DOWNLOAD), null));
}
}
});
}
/**
* {@inheritDoc}
*/
@Override
public void isCached(FileVersionDTO fileVersionDTO, final AsyncCallback<Boolean> callback) {
fileDataAsyncDAO.getByFileVersionId(fileVersionDTO.getId(), new AsyncCallback<FileDataJS>() {
@Override
public void onFailure(Throwable caught) {
callback.onFailure(caught);
}
@Override
public void onSuccess(FileDataJS fileDataJS) {
callback.onSuccess(fileDataJS != null);
}
});
}
/**
* Convert <code>data</code> into a dataUrl and start the download from the
* client browser.
*
* @param fileVersion Information about the downloaded file.
* @param data Array of bytes containing the data of the downloaded file.
*/
private void startDownload(final FileVersionDTO fileVersion, final Int8Array data) {
final JsArray<Int8Array> array = Values.createTypedJavaScriptArray(Int8Array.class);
array.push(data);
final Blob blob = Blob.createBlob(array, FileType.fromExtension(fileVersion.getExtension(), FileType._DEFAULT).getContentType());
final FileReader fileReader = new FileReader();
fileReader.addLoadFileListener(new LoadFileAdapter() {
@Override
public void onLoad() {
startDownload(fileVersion, fileReader.getResultAsString());
}
});
fileReader.readAsDataURL(blob);
}
/**
* Ask the browser to download the given file.
*
* @param fileVersion Information about the downloaded file.
* @param dataUrl Content of the file as a data URL.
*/
private void startDownload(FileVersionDTO fileVersion, String dataUrl) {
Files.startDownload(fileVersion.getName() + '.' + fileVersion.getExtension(), dataUrl);
}
public void onDownloadComplete(FileVersionDTO fileVersionDTO, Int8Array data, boolean startDownload, TransfertThread transfertThread) {
final FileVersionJS fileVersionJS = FileVersionJS.toJavaScript(fileVersionDTO);
fileDataAsyncDAO.saveOrUpdate(FileDataJS.createFileDataJS(fileVersionJS, data));
if(startDownload) {
startDownload(fileVersionDTO, data);
}
nextDownload();
}
/**
* {@inheritDoc}
*/
@Override
public void canDownload(FileVersionDTO fileVersionDTO, final AsyncCallback<Boolean> callback) {
if(state == ApplicationState.ONLINE) {
callback.onSuccess(fileVersionDTO.isAvailable());
} else {
fileDataAsyncDAO.getByFileVersionId(fileVersionDTO.getId(), new AsyncCallback<FileDataJS>() {
@Override
public void onFailure(Throwable caught) {
callback.onFailure(caught);
}
@Override
public void onSuccess(FileDataJS result) {
callback.onSuccess(result != null);
}
});
}
}
/**
* {@inheritDoc}
*/
@Override
public int getDownloadQueueSize() {
int size = downloads.size();
if(currentTasks[TransfertType.DOWNLOAD.ordinal()] > 0) {
size -= currentTasks[TransfertType.DOWNLOAD.ordinal()];
}
return size;
}
// Uploads -----------------------------------------------------------------
/**
* {@inheritDoc}
*/
@Override
public void upload(FormPanel formPanel, final ProgressListener progressListener) {
final HashMap<String, String> properties = new HashMap<String, String>();
Blob blob = null;
for(final Field<?> field : formPanel.getFields()) {
if(field instanceof ButtonFileUploadField) {
final ButtonFileUploadField fileField = (ButtonFileUploadField) field;
if(blob != null) {
throw new IllegalStateException("Multiple files have been found in the given form.");
}
blob = Blob.getBlob(fileField);
} else if(field.getName() != null && field.getValue() instanceof String) {
// BUGFIX #781: Ignoring fields with invalid values to avoid serialization errors when synchronizing.
properties.put(field.getName(), (String) field.getValue());
}
}
if(blob == null) {
throw new IllegalStateException("No file have been found in the given form.");
}
prepareFileUpload(blob, properties, progressListener);
}
@Override
public void uploadAvatar(FormPanel formPanel, ProgressListener progressListener) {
// TODO: Make it offline
// The previous system based on TransfertJS is too closely related to versioned files which is not used by contacts
directTransfertManager.uploadAvatar(formPanel, progressListener);
}
/**
* {@inheritDoc}
*/
@Override
public void resumeUpload(final TransfertJS transfertJS) {
queueTransfert(transfertJS, new ProgressAdapter() {
// No action
});
}
private void prepareFileUpload(final Blob blob, final Map<String, String> properties, final ProgressListener progressListener) {
final String fileName = blob.getName();
dispatchAsync.execute(new PrepareFileUpload(fileName, blob.getSize(), properties), new AsyncCallback<FileVersionDTO>() {
@Override
public void onFailure(Throwable caught) {
Log.error("An error occured while preparing the upload of file '" + fileName + "'.", caught);
progressListener.onFailure(Cause.SERVER_ERROR);
}
@Override
public void onSuccess(final FileVersionDTO fileVersion) {
final TransfertJS transfertJS = TransfertJS.createTransfertJS(fileVersion, TransfertType.UPLOAD);
final FileReader fileReader = new FileReader();
transfertJS.setProperties(properties);
fileReader.addLoadFileListener(new LoadFileAdapter() {
@Override
public void onLoad() {
final ArrayBuffer arrayBuffer = fileReader.getResultAsArrayBuffer();
final Int8Array int8Array = Int8Array.createInt8Array(arrayBuffer);
transfertJS.setData(int8Array);
final FileDataJS fileDataJS = Values.createJavaScriptObject(FileDataJS.class);
fileDataJS.setData(int8Array);
fileDataJS.setMimeType(blob.getType());
if (fileVersion != null) {
fileDataJS.setFileVersion(FileVersionJS.toJavaScript(fileVersion));
}
fileDataAsyncDAO.saveOrUpdate(fileDataJS);
transfertAsyncDAO.saveOrUpdate(transfertJS, new AsyncCallback<TransfertJS>() {
@Override
public void onFailure(Throwable caught) {
N10N.offlineNotif(I18N.CONSTANTS.error(), I18N.CONSTANTS.offlineTransfertUploadStoreError(), eventBus);
queueTransfert(transfertJS, progressListener);
}
@Override
public void onSuccess(TransfertJS result) {
queueTransfert(transfertJS, progressListener);
}
});
}
@Override
public void onError() {
progressListener.onFailure(Cause.BLOB_READ_ERROR);
}
});
fileReader.readAsArrayBuffer(blob);
}
});
}
public void onUploadComplete(TransfertThread transfertThread) {
nextUpload();
}
/**
* {@inheritDoc}
*/
@Override
public boolean canUpload() {
return true;
}
/**
* {@inheritDoc}
*/
@Override
public int getUploadQueueSize() {
int size = uploads.size();
if(currentTasks[TransfertType.UPLOAD.ordinal()] > 0) {
size -= currentTasks[TransfertType.UPLOAD.ordinal()];
}
return size;
}
// Global Progress ---------------------------------------------------------
/**
* {@inheritDoc}
*/
@Override
public void setProgressListener(TransfertType type, ProgressListener progressListener) {
progressListeners.put(type, progressListener);
}
/**
* {@inheritDoc}
*/
@Override
public void removeProgressListener(TransfertType type) {
progressListeners.remove(type);
}
public void onProgress() {
fireProgress();
}
protected void fireProgress() {
fireProgress(progressListeners.get(TransfertType.DOWNLOAD), downloadThread, downloads, TransfertType.DOWNLOAD);
fireProgress(progressListeners.get(TransfertType.UPLOAD), uploadThread, uploads, TransfertType.UPLOAD);
}
private void fireProgress(ProgressListener progressListener, TransfertThread thread, List<Task> tasks, TransfertType type) {
if(progressListener != null && currentTasks[type.ordinal()] >= 0) {
double progress = currentTasks[type.ordinal()];
double speed = 0.0;
if(thread.getTask() != null) {
progress += thread.getProgress();
speed = thread.getSpeed();
}
progress /= tasks.size();
progressListener.onProgress(progress, speed);
}
}
}