package org.geogebra.web.web.main;
import java.util.ArrayList;
import java.util.TreeSet;
import org.geogebra.common.main.App;
import org.geogebra.common.main.Feature;
import org.geogebra.common.main.Localization;
import org.geogebra.common.main.MaterialsManager;
import org.geogebra.common.move.ggtapi.models.JSONParserGGT;
import org.geogebra.common.move.ggtapi.models.Material;
import org.geogebra.common.move.ggtapi.models.Material.MaterialType;
import org.geogebra.common.move.ggtapi.models.MaterialFilter;
import org.geogebra.common.move.ggtapi.models.SyncEvent;
import org.geogebra.common.plugin.Event;
import org.geogebra.common.plugin.EventType;
import org.geogebra.common.util.AsyncOperation;
import org.geogebra.common.util.debug.Log;
import org.geogebra.web.html5.Browser;
import org.geogebra.web.html5.main.AppW;
import org.geogebra.web.html5.main.StringHandler;
import org.geogebra.web.web.util.SaveCallback;
import org.geogebra.web.web.util.SaveCallback.SaveState;
import com.google.gwt.storage.client.Storage;
/**
* Manager for files from {@link Storage localStorage}
*
* JSON including the base64 and metadata is stored under
* "file_<local id>_<title>" key. The id field inside JSON is for Tube id, is
* not affected by local id. Local id can still be found inside title => we need
* to extract title after we load file from LS.
*
*/
public class FileManagerW extends FileManager {
private static final String TIMESTAMP = "timestamp";
/** locale storage */
Storage stockStore = Storage.getLocalStorageIfSupported();
/**
* @param app
* {@link AppW}
*/
public FileManagerW(final AppW app) {
super(app);
}
@Override
public void delete(final Material mat, boolean permanent, Runnable onSuccess) {
if (this.stockStore != null) {
this.stockStore.removeItem(getFileKey(mat));
removeFile(mat);
onSuccess.run();
}
}
@Override
public void saveFile(String base64, long modified, final SaveCallback cb) {
if (this.stockStore == null) {
return;
}
final Material mat = createMaterial(base64, modified);
int id;
if (getApp().getLocalID() == -1) {
id = createID();
getApp().setLocalID(id);
} else {
id = getApp().getLocalID();
}
String key = createKeyString(id, getApp().getKernel().getConstruction()
.getTitle());
updateViewerId(mat);
mat.setLocalID(id);
try {
stockStore.setItem(key, mat.toJson().toString());
cb.onSaved(mat, true);
} catch (Exception e) {
if (cb.getState() != SaveState.ERROR) {
mat.setSyncStamp(-1);
refreshMaterial(mat);
}
cb.onError();
}
}
private void updateViewerId(Material mat) {
if (app.getLoginOperation() != null
&& app.getLoginOperation().getModel() != null) {
mat.setViewerID(app.getLoginOperation().getModel().getUserId());
}
}
/**
* creates a new ID
*
* @return int ID
*/
int createID() {
int nextFreeID = 1;
for (int i = 0; i < this.stockStore.getLength(); i++) {
final String key = this.stockStore.key(i);
if (key.startsWith(MaterialsManager.FILE_PREFIX)) {
int fileID = MaterialsManager.getIDFromKey(key);
if (fileID >= nextFreeID) {
nextFreeID = MaterialsManager.getIDFromKey(key) + 1;
}
}
}
return nextFreeID;
}
@Override
protected void getFiles(final MaterialFilter filter) {
if (this.stockStore == null || this.stockStore.getLength() <= 0) {
return;
}
for (int i = 0; i < this.stockStore.getLength(); i++) {
final String key = this.stockStore.key(i);
if (key.startsWith(MaterialsManager.FILE_PREFIX)) {
Material mat = JSONParserGGT.parseMaterial(this.stockStore
.getItem(key));
if (mat == null) {
mat = new Material(0, MaterialType.ggb);
mat.setTitle(getTitleFromKey(key));
}
if (filter.check(mat)
&& app.getLoginOperation().mayView(mat)) {
addMaterial(mat);
}
}
}
}
@Override
public void uploadUsersMaterials(final ArrayList<SyncEvent> events) {
if (this.stockStore == null || this.stockStore.getLength() <= 0) {
return;
}
ArrayList<String> keys = new ArrayList<String>();
for (int i = 0; i < this.stockStore.getLength(); i++) {
keys.add(this.stockStore.key(i));
}
setNotSyncedFileCount(keys.size(), events);
for (int i = 0; i < keys.size(); i++) {
final String key = keys.get(i);
if (key.startsWith(FILE_PREFIX)) {
final Material mat = JSONParserGGT
.parseMaterial(this.stockStore.getItem(key));
if (getApp().getLoginOperation().owns(mat)) {
sync(mat, events);
} else {
ignoreNotSyncedFile(events);
}
} else {
ignoreNotSyncedFile(events);
}
}
}
private int freeBytes = -1;
private TreeSet<Integer> offlineIDs = new TreeSet<Integer>();
@Override
public boolean shouldKeep(int id) {
if (!getApp().has(Feature.LOCALSTORAGE_FILES)) {
return false;
}
if (offlineIDs.contains(id)) {
return true;
}
if (freeBytes == -1) {
countFreeBytes();
}
if (freeBytes > 10e6) {
return true;
}
return false;
}
private void countFreeBytes() {
if (this.stockStore == null) {
return;
}
this.freeBytes = 5000000;
}
@Override
public void rename(String newTitle, Material mat) {
rename(newTitle, mat, null);
}
@Override
public void rename(String newTitle, Material mat, Runnable callback) {
if (this.stockStore == null) {
return;
}
this.stockStore.removeItem(mat.getTitle());
int newID = createID();
mat.setLocalID(createID());
mat.setTitle(newTitle);
this.stockStore.setItem(
MaterialsManager.createKeyString(newID, newTitle),
mat.toJson().toString());
}
@Override
public void autoSave(int counter) {
if (this.stockStore == null || counter % 30 != 0) {
return;
}
final StringHandler base64saver = new StringHandler() {
@Override
public void handle(final String s) {
final Material mat = createMaterial(s,
System.currentTimeMillis() / 1000);
try {
stockStore.setItem(AUTO_SAVE_KEY, mat.toJson().toString());
} catch (Exception e) {
Log.warn("Autosave failed");
}
}
};
getApp().getGgbApi().getBase64(true, base64saver);
}
@Override
public String getAutosaveJSON() {
if (Browser.supportsSessionStorage()) {
if (stockStore != null) {
if (stockStore.getItem(TIMESTAMP) != null) {
long l = 0;
try{
l = Long.parseLong(stockStore.getItem(TIMESTAMP));
} catch (Exception e) {
Log.warn("Invalid timestamp.");
}
if (l > System.currentTimeMillis() - 2000) {
Log.debug(
"App still running, autosave timestamp: " + l);
return null;
}
}
return stockStore.getItem(AUTO_SAVE_KEY);
}
}
return null;
}
/**
* opens the auto-saved file. call first {@code isAutoSavedFileAvailable}
*/
@Override
public void restoreAutoSavedFile(String materialJSON) {
Material autoSaved = JSONParserGGT
.parseMaterial(materialJSON);
// maybe another user restores the file, so reset
// sensitive data
autoSaved.setAuthor("");
autoSaved.setAuthorId(0);
autoSaved.setId(0);
autoSaved.setGoogleID("");
openMaterial(autoSaved);
}
@Override
public void deleteAutoSavedFile() {
if (this.stockStore == null) {
return;
}
this.stockStore.removeItem(AUTO_SAVE_KEY);
}
@Override
public void saveLoggedOut(App app1) {
((AppW) app1).getGuiManager().exportGGB();
}
@Override
public void export(App app1) {
((AppW) app1).getGuiManager().exportGGB();
}
@Override
public void setTubeID(String localID, Material mat) {
if (this.stockStore == null) {
return;
}
final Material oldMat = JSONParserGGT
.parseMaterial(this.stockStore
.getItem(localID));
mat.setBase64(oldMat.getBase64());
updateViewerId(mat);
try {
this.stockStore.setItem(localID, mat.toJson().toString());
} catch (Exception e) {
Log.warn("setting tube ID failed");
}
this.offlineIDs.add(mat.getId());
}
@Override
protected void updateFile(String localKey, long modified, Material material) {
if (this.stockStore == null) {
return;
}
material.setModified(modified);
material.setSyncStamp(modified);
String key = localKey;
if (key == null) {
key = MaterialsManager.createKeyString(this.createID(),
material.getTitle());
}
try {
this.stockStore.setItem(key, material.toJson().toString());
this.offlineIDs.add(material.getId());
} catch (Exception e) {
Log.warn("Updating local copy failed.");
}
}
@Override
public void showExportAsPictureDialog(final String url, String filename,
final App app1) {
String extension = "png";
String titleKey = "ExportAsPicture";
if (url.startsWith("data:text/")) {
// eg Tizk export
extension = "txt";
titleKey = "Export";
// could use:
// TitleExportPstricks
// TitleExportPgf
}
final String extension2 = extension;
Localization loc = getApp().getLocalization();
((AppW) app1).getGuiManager()
.getOptionPane()
.showSaveDialog(getApp(), loc.getMenu(titleKey),
filename + "." + extension, null,
new AsyncOperation<String[]>() {
@Override
public void callback(String[] obj) {
if (Integer.parseInt(obj[0]) != 0) {
return;
}
exportImage(url, obj[1], extension2);
getApp().dispatchEvent(new Event(
EventType.EXPORT, null,
"[\"" + extension2 + "\"]"));
}
}, loc.getMenu("Export"));
}
@Override
public boolean hasBase64(Material material) {
return material.getBase64() != null
&& material.getBase64().length() > 0;
}
@Override
public void exportImage(String url, String filename, String extension) {
Browser.exportImage(url, filename);
}
@Override
public void refreshAutosaveTimestamp() {
if (stockStore != null) {
stockStore.setItem(TIMESTAMP, "" + System.currentTimeMillis());
}
}
}