package com.gwt.ui.client.gwtupld;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.EventTarget;
import com.google.gwt.dom.client.InputElement;
import com.google.gwt.event.dom.client.ChangeEvent;
import com.google.gwt.event.dom.client.ChangeHandler;
import com.google.gwt.event.dom.client.DragEnterEvent;
import com.google.gwt.event.dom.client.DragEnterHandler;
import com.google.gwt.event.dom.client.DragLeaveEvent;
import com.google.gwt.event.dom.client.DragLeaveHandler;
import com.google.gwt.event.dom.client.DragOverEvent;
import com.google.gwt.event.dom.client.DragOverHandler;
import com.google.gwt.event.dom.client.DropEvent;
import com.google.gwt.event.dom.client.DropHandler;
import com.google.gwt.event.shared.GwtEvent;
import com.google.gwt.event.shared.HandlerManager;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.event.shared.HasHandlers;
import com.google.gwt.json.client.JSONArray;
import com.google.gwt.json.client.JSONObject;
import com.google.gwt.json.client.JSONValue;
import com.google.gwt.user.client.ui.Widget;
import com.gwt.ui.client.gwtupld.events.UploadingCompletedEvent;
import com.gwt.ui.client.gwtupld.events.UploadingCompletedEventHandler;
import com.gwt.ui.client.gwtupld.file.File;
import com.gwt.ui.client.gwtupld.file.FileList;
import com.gwt.ui.client.gwtupld.shr.DataTransferAdvanced;
import com.gwt.ui.client.gwtupld.utils.UUID;
public abstract class FileUploader extends Widget implements UploadProgressHandlers, HasHandlers {
private HandlerManager handlerManager;
private UploadButton uploadButton;
private InputElement fileInput;
private Options options;
private int filesInProgress = 0;
protected UploadHandlerAbstract uploadHandler;
protected Map<String, FileInfo> fileInfos;
protected abstract Element getDropZone();
/**
* Updates resulting file list when file is added to queue, finished uploading or set via updateView.
*
* @param justAddedId id is required in order not to update the whole container, but just the exact item
*/
protected abstract void updateExactFileInfo(String justAddedId);
protected abstract void onDropEvent();
protected abstract void onDragLeaveEvent();
protected abstract void onDragEnterEvent();
public abstract void updateView(List<FileInfo> files);
public FileUploader(Options options) {
handlerManager = new HandlerManager(this);
this.options = options;
fileInfos = new HashMap<String, FileInfo>();
uploadHandler = createUploadHandler();
}
@Override
public void fireEvent(GwtEvent<?> event) {
handlerManager.fireEvent(event);
super.fireEvent(event);
}
public HandlerRegistration addUploadingCompletedEventHandler(UploadingCompletedEventHandler handler) {
return handlerManager.addHandler(UploadingCompletedEvent.TYPE, handler);
}
/**
* Must be called after Binder.createAndBindUi. How to ensure that it was actually called from a subclass?
*/
protected final void initialize() {
getDropZone().setInnerText(options.useAdvancedUploader() ? "Drop files here or click to attach" : "Click to attach files");
fileInput = createUploadButton(getDropZone());
// TODO: what is this for?
// preventLeaveInProgress();
addInputChangeHandlers();
addDragAndDropHandlers();
}
public Options getOptions() {
return options;
}
/**
* Used to get control state.
*
* @return FileInfos by id
*/
public Map<String, FileInfo> getFileInfos() {
return fileInfos;
}
private UploadHandlerAbstract createUploadHandler() {
final UploadHandlerAbstract uploadHandler;
if (options.useAdvancedUploader()) {
uploadHandler = new UploadHandlerXhr(this, options);
} else {
uploadHandler = new UploadHandlerForm(this, options);
}
return uploadHandler;
}
private FileInputElement createUploadButton(Element container) {
if (uploadButton != null) {
uploadButton.reset();
}
uploadButton = new UploadButton(container, options.isMultiple() && options.useAdvancedUploader());
return uploadButton.getInput();
}
private void addInputChangeHandlers() {
addDomHandler(new ChangeHandler() {
@Override
public void onChange(ChangeEvent event) {
final EventTarget et = event.getNativeEvent().getEventTarget();
final Element el = Element.as(et);
if (el == fileInput) {
onInputChange((FileInputElement)fileInput);
}
}
}, ChangeEvent.getType());
}
private void onInputChange(FileInputElement input) {
if (uploadHandler instanceof UploadHandlerXhr) {
uploadFileList(input.getFiles());
} else {
if (validateFile(input)) {
uploadFile(input);
}
}
fileInput = createUploadButton(getDropZone());
}
private void uploadFileList(FileList files) {
for (File file : files) {
if (validateFile(file)) {
uploadFile(file);
}
}
}
private void uploadFile(Object file) {
String id = uploadHandler.add(file);
String fileName = uploadHandler.getName(id);
// if (onSubmit(id, fileName) > 0) {
onSubmit(id, fileName);
uploadHandler.upload(id, options.getParams());
// }
}
private boolean validateFile(FileInputElement input) {
String name = input.getValue().replaceAll(".*(\\/|\\\\)", "");
int size = -1;
return validate(name, size);
}
private boolean validateFile(File file) {
String name = file.getName();
int size = file.getSize();
return validate(name, size);
}
private boolean validate(String name, int size) {
final String error = validateFile(name, size);
if (error != null) {
addFileToList(UUID.uuid(), name, size, error);
}
return error == null;
}
/**
* Checks file for being valid for upload
*
* @param name file name
* @param size file size
* @return Error cause or null if OK
*/
private String validateFile(String name, int size) {
if (!isAllowedExtension(name)) {
return "Click to attach files" + options.getAllowedExtensions().toString();
}
if (size == 0) {
return "File size is 0";
}
if (size > 0 && options.getMinSize() > 0 && size < options.getMinSize()) {
return "File size should be more than " + formatSize(options.getMinSize());
}
if (size > 0 && options.getMaxSize() > 0 && size > options.getMaxSize()) {
return "File size should be less than" + formatSize(options.getMaxSize());
}
return null;
}
private boolean isAllowedExtension(String name) {
if (options.getAllowedExtensions() == null || options.getAllowedExtensions().size() == 0) {
return true;
}
if (name == null || name.lastIndexOf('.') == -1) {
return false;
}
String ext = name.substring(name.lastIndexOf('.') + 1).toLowerCase();
return options.getAllowedExtensions().contains(ext);
}
private void addDragAndDropHandlers() {
if (options.useAdvancedUploader()) {
addDomHandler(new DropHandler() {
@Override
public void onDrop(DropEvent event) {
final EventTarget eventTarget = event.getNativeEvent().getEventTarget();
Element el = Element.as(eventTarget);
if (el == fileInput) {
DataTransferAdvanced dta = (DataTransferAdvanced)event.getDataTransfer();
uploadFileList(dta.getFiles());
onDropEvent();
}
event.preventDefault();
}
}, DropEvent.getType());
addDomHandler(new DragOverHandler() {
@Override
public void onDragOver(DragOverEvent event) {
DataTransferAdvanced dta = (DataTransferAdvanced)event.getDataTransfer();
String effect = dta.getEffectAllowed();
if (effect.equals("move") || effect.equals("linkmove")) {
// for FF (only move allowed)
dta.setDropEffect("move");
} else {
// for Chrome
dta.setDropEffect("copy");
}
event.stopPropagation();
event.preventDefault();
}
}, DragOverEvent.getType());
addDomHandler(new DragEnterHandler() {
@Override
public void onDragEnter(DragEnterEvent event) {
final EventTarget eventTarget = event.getNativeEvent().getEventTarget();
Element el = Element.as(eventTarget);
if (el == fileInput) {
onDragEnterEvent();
}
}
}, DragEnterEvent.getType());
addDomHandler(new DragLeaveHandler() {
@Override
public void onDragLeave(DragLeaveEvent event) {
final EventTarget eventTarget = event.getNativeEvent().getEventTarget();
Element el = Element.as(eventTarget);
if (el == fileInput) {
onDragLeaveEvent();
}
}
}, DragLeaveEvent.getType());
}
}
protected String formatSize(int bytes) {
if (bytes == -1) {
return "";
}
final int kbSize = 1024;
final int mbSize = kbSize * kbSize;
boolean mb = false;
int divideBy = kbSize;
if (bytes > mbSize) {
mb = true;
divideBy = mbSize;
}
final String s = String.valueOf((double)bytes / divideBy);
int endIndex = s.indexOf('.') + 2;
if (endIndex > s.length()) {
endIndex = s.length() - 1;
}
return s.substring(0, endIndex) + " " + (mb ? "MB" : "KB");
}
@Override
public void onComplete(String id, String filename, JSONValue response) {
if (response.isArray() != null) {
JSONArray array = (JSONArray)response;
for (int i = 0; i < array.size(); i++) {
filesInProgress--;
if (filesInProgress == 0) {
fireEvent(new UploadingCompletedEvent());
}
final JSONObject value = (JSONObject)array.get(i);
final JSONValue size = value.get("size");
final JSONValue url = value.get("url");
final JSONValue type = value.get("type");
final String url2 = String.valueOf(url);
fileInfos.put(id, new FileInfo(id,
// TODO: is there a good way to remove quotes?
url2.substring(1, url2.length() - 1), filename, Integer.valueOf(String.valueOf(size)), Integer.valueOf(String.valueOf(size)), String.valueOf(type), null, false, value));
updateExactFileInfo(id);
}
}
}
private native static boolean preventLeaveInProgress() /*-{
var self = this;
qq.attach(window, 'beforeunload', function(e){
if (!self._filesInProgress){return;}
var e = e || window.event;
// for ie, ff
e.returnValue = self._options.messages.onLeave;
// for webkit
return self._options.messages.onLeave;
});
}-*/;
// original comment said 'return false to cancel submit'?! wtf
@Override
public int onSubmit(String id, String filename) {
filesInProgress++;
addFileToList(id, filename, -1, null);
return filesInProgress;
}
private void addFileToList(String id, String filename, int size, String error) {
fileInfos.put(id, new FileInfo(id, null, filename, size, -1, null, error, false, null));
updateExactFileInfo(id);
}
@Override
public void onProgress(String id, String filename, int loaded, int total) {
fileInfos.put(id, new FileInfo(id, null, filename, total, loaded, null, null, false, null));
updateExactFileInfo(id);
}
private void initAddFileInfo(FileInfo file) {
final String uuid = UUID.uuid();
fileInfos.put(uuid, file);
updateExactFileInfo(uuid);
}
protected void initAddFileInfos(List<FileInfo> files) {
if (files != null) {
for (FileInfo file : files) {
initAddFileInfo(file);
}
}
}
/**
* Gets total number of files to be uploaded. Excluding those ones having error.
*
* @return total number of files to be uploaded
*/
protected int filesDueToUpload() {
int result = 0;
for (FileInfo fileInfo : fileInfos.values()) {
if (fileInfo.dueToUpload()) {
result++;
}
}
return result;
}
/**
* Gets total number of files already uploaded.
*
* @return total number of files already uploaded
*/
protected int filesUploaded() {
int result = 0;
for (FileInfo fileInfo : fileInfos.values()) {
if (fileInfo.dueToUpload() && fileInfo.uploadingHasFinished()) {
result++;
}
}
return result;
}
/**
* Calculates total progress of all the files being uploaded
*
* @return total progress of all the files being uploaded
*/
protected int totalProgress() {
int result = 0;
for (FileInfo fileInfo : fileInfos.values()) {
if (fileInfo.dueToUpload()) {
result += fileInfo.getPercentageReady();
}
}
return result / filesDueToUpload();
}
}