package floobits.common;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import floobits.common.interfaces.IContext;
import floobits.common.interfaces.IDoc;
import floobits.common.interfaces.IFile;
import floobits.common.protocol.FlooPatch;
import floobits.common.protocol.FlooUser;
import floobits.common.protocol.buf.BinaryBuf;
import floobits.common.protocol.buf.Buf;
import floobits.common.protocol.buf.TextBuf;
import floobits.common.protocol.handlers.FlooHandler;
import floobits.common.protocol.json.receive.*;
import floobits.common.protocol.json.send.CreateBufResponse;
import floobits.common.protocol.json.send.RoomInfoResponse;
import floobits.utilities.Flog;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Type;
import java.util.*;
public class InboundRequestHandler {
private IContext context;
private final FloobitsState state;
private final OutboundRequestHandler outbound;
private boolean shouldUpload;
private StatusMessageThrottler fileAddedMessageThrottler;
private StatusMessageThrottler fileRemovedMessageThrottler;
private EditorScheduler editor;
enum Events {
room_info, get_buf, patch, highlight, saved, join, part, create_buf, ack,
request_perms, msg, rename_buf, term_stdin, term_stdout, delete_buf, perms, ping
}
public InboundRequestHandler(IContext context, FloobitsState state, OutboundRequestHandler outbound, boolean shouldUpload) {
this.context = context;
editor = context.editor;
this.state = state;
this.outbound = outbound;
this.shouldUpload = shouldUpload;
fileAddedMessageThrottler = new StatusMessageThrottler(context,
"%d files were added to the workspace.");
fileRemovedMessageThrottler = new StatusMessageThrottler(context,
"%d files were removed from the workspace.");
}
private void initialManageConflicts(RoomInfoResponse ri) {
final LinkedList<Buf> conflicts = new LinkedList<Buf>();
final LinkedList<Buf> missing = new LinkedList<Buf>();
final LinkedList<String> conflictedPaths = new LinkedList<String>();
for (Map.Entry entry : ri.bufs.entrySet()) {
Integer buf_id = (Integer) entry.getKey();
RoomInfoBuf b = (RoomInfoBuf) entry.getValue();
Buf buf = Buf.createBuf(b.path, b.id, Encoding.from(b.encoding), b.md5, context, outbound);
if (state.bufs == null) {
Flog.warn("Buffer list became null. Probably disconnected. Bailing.");
return;
}
state.bufs.put(buf_id, buf);
state.pathsToIds.put(b.path, b.id);
buf.read();
if (buf.buf == null) {
if (buf.path.equals("FLOOBITS_README.md") && buf.id == 1) {
outbound.getBuf(buf.id);
continue;
}
missing.add(buf);
conflictedPaths.add(buf.path);
continue;
}
if (!b.md5.equals(buf.md5)) {
conflicts.add(buf);
conflictedPaths.add(buf.path);
}
}
if (conflictedPaths.size() <= 0) {
return;
}
String[] conflictedPathsArray = conflictedPaths.toArray(new String[conflictedPaths.size()]);
Runnable stompLocal = new Runnable() {
@Override
public void run() {
for (Buf buf : conflicts) {
outbound.getBuf(buf.id);
}
for (Buf buf : missing) {
outbound.getBuf(buf.id);
}
}
};
Runnable stompRemote = new Runnable() {
@Override
public void run() {
for (Buf buf : conflicts) {
outbound.setBuf(buf);
outbound.saveBuf(buf);
}
for (Buf buf : missing) {
outbound.deleteBuf(buf, false);
}
}
};
Runnable flee = new Runnable() {
@Override
public void run() {
context.shutdown();
}
};
LinkedList<String> connectedUsersList = new LinkedList<String>();
for (Map.Entry<Integer, FlooUser> userEntry : state.users.entrySet()) {
FlooUser user = userEntry.getValue();
connectedUsersList.add(String.format("%s, %s", user.username, user.client));
}
context.dialogResolveConflicts(stompLocal, stompRemote, state.readOnly, flee, conflictedPathsArray,
connectedUsersList.toArray(new String[connectedUsersList.size()]));
}
private void initialUpload(RoomInfoResponse ri) {
context.statusMessage("Overwriting remote files and uploading new ones.");
context.flashMessage("Overwriting remote files and uploading new ones.");
final Ignore ignoreTree = context.getIgnoreTree();
ArrayList<Ignore> allIgnores = new ArrayList<Ignore>();
LinkedList<Ignore> tempIgnores = new LinkedList<Ignore>();
tempIgnores.add(ignoreTree);
int size = 0;
Ignore ignore;
while (tempIgnores.size() > 0) {
ignore = tempIgnores.removeLast();
size += ignore.size;
allIgnores.add(ignore);
for(Ignore ig: ignore.children.values()) {
tempIgnores.add(ig);
}
}
LinkedList<Ignore> tooBigIgnores = new LinkedList<Ignore>();
Collections.sort(allIgnores);
while (size > ri.max_size) {
Ignore ig = allIgnores.remove(0);
size -= ig.size;
tooBigIgnores.add(ig);
}
if (tooBigIgnores.size() > 0) {
if (tooBigIgnores.size() > Constants.TOO_MANY_BIG_DIRS) {
context.dialogDisconnect(ri.max_size/1000, tooBigIgnores.size());
return;
}
boolean shouldContinue;
shouldContinue = context.dialogTooBig(tooBigIgnores);
if (!shouldContinue) {
context.shutdown();
return;
}
}
HashSet<String> paths = new HashSet<String>();
for (Ignore ig : allIgnores) {
for (IFile virtualFile : ig.files)
paths.add(context.toProjectRelPath(virtualFile.getPath()));
}
for (Map.Entry entry : ri.bufs.entrySet()) {
Integer buf_id = (Integer) entry.getKey();
RoomInfoBuf b = (RoomInfoBuf) entry.getValue();
Buf buf = Buf.createBuf(b.path, b.id, Encoding.from(b.encoding), b.md5, context, outbound);
try {
state.bufs.put(buf_id, buf);
} catch (NullPointerException e) {
Flog.warn("state.buffs is null, tried to upload after disconnecting. This is a race condition.");
return;
}
state.pathsToIds.put(b.path, b.id);
if (!paths.contains(buf.path)) {
outbound.deleteBuf(buf, false);
continue;
}
paths.remove(buf.path);
buf.read();
if (buf.buf == null) {
Flog.warn("%s is null but we want to upload it?", b.path);
outbound.getBuf(buf.id);
continue;
}
if (b.md5.equals(buf.md5)) {
continue;
}
outbound.setBuf(buf);
outbound.saveBuf(buf);
}
for (String path : paths) {
IFile fileByPath = context.iFactory.findFileByPath(context.absPath(path));
if (fileByPath == null || !fileByPath.isValid()) {
Flog.warn(String.format("path is no longer a valid virtual file"));
continue;
}
outbound.createBuf(fileByPath);
}
String flooignore = FilenameUtils.concat(context.colabDir, ".flooignore");
try {
File f = new File(flooignore);
List<String> strings;
if (f.exists()) {
strings = FileUtils.readLines(f);
} else {
strings = new ArrayList<String>();
}
for (Ignore ig : tooBigIgnores) {
String rule = "/" + context.toProjectRelPath(ig.stringPath);
if (!rule.endsWith("/")) {
rule += "/";
}
rule += "*";
strings.add(rule);
}
context.setListener(false);
FileUtils.writeLines(f, strings);
IFile fileByIoFile = context.iFactory.findFileByIoFile(f);
if (fileByIoFile != null) {
fileByIoFile.refresh();
ignoreTree.addRules(fileByIoFile);
}
} catch (IOException e) {
Flog.warn(e);
} finally {
context.setListener(true);
}
shouldUpload = false;
}
void _on_rename_buf(JsonObject jsonObject) {
final String name = jsonObject.get("old_path").getAsString();
final String oldPath = context.absPath(name);
final String newPath = context.absPath(jsonObject.get("path").getAsString());
Buf buf = state.getBufByPath(oldPath);
if (buf == null) {
if (state.getBufByPath(newPath) == null) {
Flog.warn("Rename oldPath and newPath don't exist. %s %s", oldPath, newPath);
} else {
Flog.info("We probably rename this, nothing to rename.");
}
return;
}
editor.queue(buf, new RunLater<Buf>() {
@Override
public void run(Buf buf) {
final IFile foundFile = context.iFactory.findFileByPath(oldPath);
if (foundFile == null) {
Flog.warn("File we want to move was not found %s %s.", oldPath, newPath);
return;
}
String newRelativePath = context.toProjectRelPath(newPath);
if (newRelativePath == null) {
context.errorMessage("A file is now outside the workspace.");
return;
}
state.setBufPath(buf, newRelativePath);
File oldFile = new File(oldPath);
File newFile = new File(newPath);
String newFileName = newFile.getName();
// Rename file
if (foundFile.rename(null, newFileName)) {
return;
}
// Move file
String newParentDirectoryPath = newFile.getParent();
String oldParentDirectoryPath = oldFile.getParent();
if (newParentDirectoryPath.equals(oldParentDirectoryPath)) {
Flog.warn("Only rename file, don't need to move %s %s", oldPath, newPath);
return;
}
IFile directory = context.iFactory.createDirectories(newParentDirectoryPath);
if (directory == null) {
return;
}
foundFile.move(null, directory);
}
}
);
}
void _on_request_perms(JsonObject obj) {
Flog.log("got perms receive %s", obj);
RequestPerms requestPerms = new Gson().fromJson(obj, (Type)RequestPerms.class);
final int userId = requestPerms.user_id;
final FlooUser u = state.getUser(userId);
if (u == null) {
Flog.info("Unknown user for id %s. Not handling request_perms event.", userId);
return;
}
context.dialogPermsRequest(u.username, new RunLater<String>() {
@Override
public void run(String action) {
outbound.setPerms(action, userId, new String[]{"edit_room"});
}
});
}
void _on_join(JsonObject obj) {
FlooUser u = new Gson().fromJson(obj, (Type) FlooUser.class);
state.addUser(u);
}
void _on_part(JsonObject obj) {
JsonElement id = obj.get("user_id");
if (id == null){
return;
}
Integer userId = id.getAsInt();
state.removeUser(userId);
context.iFactory.removeHighlightsForUser(userId);
}
void _on_delete_buf(JsonObject obj) {
final DeleteBuf deleteBuf = new Gson().fromJson(obj, (Type)DeleteBuf.class);
Buf buf = state.bufs.get(deleteBuf.id);
if (buf == null) {
Flog.warn(String.format("Tried to delete a buf that doesn't exist: %d", deleteBuf.id));
return;
}
editor.queue(buf, new RunLater<Buf>() {
@Override
public void run(Buf buf) {
buf.cancelTimeout();
if (state.bufs != null) {
state.bufs.remove(deleteBuf.id);
state.pathsToIds.remove(buf.path);
}
if (!deleteBuf.unlink) {
fileRemovedMessageThrottler.statusMessage(String.format("Removed the file, %s, from the workspace.", buf.path));
return;
}
String absPath = context.absPath(buf.path);
final IFile fileByPath = context.iFactory.findFileByPath(absPath);
if (fileByPath == null) {
return;
}
fileByPath.delete(this);
}
});
}
void _on_msg(JsonObject jsonObject){
String msg = jsonObject.get("data").getAsString();
String username = jsonObject.get("username").getAsString();
Double time = jsonObject.get("time").getAsDouble();
Date messageDate;
if (time == null) {
messageDate = new Date();
} else {
Calendar c = Calendar.getInstance();
c.setTimeInMillis(time.longValue() * 1000);
messageDate = c.getTime();
}
context.chat(username, msg, messageDate);
}
void _on_term_stdout(JsonObject jsonObject) {}
void _on_term_stdin(JsonObject jsonObject) {}
void _on_ping(JsonObject jsonObject) {
outbound.pong();
}
public void _on_highlight(JsonObject obj) {
final FlooHighlight res = new Gson().fromJson(obj, (Type) FlooHighlight.class);
state.lastHighlight = obj;
final Buf buf = this.state.bufs.get(res.id);
editor.queue(buf, new RunLater<Buf>() {
@Override
public void run(Buf arg) {
IDoc iDoc = context.iFactory.getDocument(buf.path);
if (iDoc == null) {
return;
}
String username = state.getUsername(res.user_id);
iDoc.applyHighlight(buf.path, res.user_id, username, state.getFollowing() && !res.following, res.summon, res.ranges);
}
});
}
void _on_saved(JsonObject obj) {
final Integer id = obj.get("id").getAsInt();
final Buf buf = this.state.bufs.get(id);
editor.queue(buf, new RunLater<Buf>() {
public void run(Buf b) {
IDoc document = context.iFactory.getDocument(buf.path);
if (document == null) {
return;
}
document.save();
}
});
}
void _on_create_buf(JsonObject obj) {
Gson gson = new Gson();
GetBufResponse res = gson.fromJson(obj, (Type) CreateBufResponse.class);
Buf buf;
if (res.encoding.equals(Encoding.BASE64.toString())) {
buf = new BinaryBuf(res.path, res.id, new Base64().decode(res.buf.getBytes()), res.md5, context, outbound);
} else {
buf = new TextBuf(res.path, res.id, res.buf, res.md5, context, outbound);
}
editor.queue(buf, new RunLater<Buf>() {
@Override
public void run(Buf buf) {
if (state.bufs == null) {
return;
}
state.bufs.put(buf.id, buf);
state.pathsToIds.put(buf.path, buf.id);
buf.write();
fileAddedMessageThrottler.statusMessage(String.format("Added the file, %s, to the workspace.", buf.path));
}
});
}
void _on_perms(JsonObject obj) {
Perms res = new Gson().fromJson(obj, (Type) Perms.class);
Boolean previousState = state.can("patch");
if (res.user_id != state.getMyConnectionId()) {
return;
}
HashSet perms = new HashSet<String>(Arrays.asList(res.perms));
if (res.action.equals("add")) {
state.perms.addAll(perms);
} else if (res.action.equals("set")) {
state.perms.clear();
state.perms.addAll(perms);
} else if (res.action.equals("remove")) {
state.perms.removeAll(perms);
}
state.readOnly = !state.can("patch");
if (state.can("patch") != previousState) {
if (state.can("patch")) {
context.statusMessage("You state.can now edit this workspace.");
context.iFactory.clearReadOnlyState();
} else {
context.errorMessage("You state.can no longer edit this workspace.");
}
}
}
void _on_patch(JsonObject obj) {
final FlooPatch res = new Gson().fromJson(obj, (Type) FlooPatch.class);
final Buf buf = this.state.bufs.get(res.id);
editor.queue(buf, new RunLater<Buf>() {
@Override
public void run(Buf b) {
if (b.buf == null) {
Flog.warn("no buffer");
outbound.getBuf(res.id);
return;
}
if (res.patch.length() == 0) {
Flog.warn("wtf? no patches to apply. server is being stupid");
return;
}
b.patch(res);
}
});
}
void _on_room_info(final JsonObject obj) {
context.readThread(new Runnable() {
@Override
public void run() {
try {
RoomInfoResponse ri = new Gson().fromJson(obj, (Type) RoomInfoResponse.class);
state.handleRoomInfo(ri);
context.statusMessage(String.format("You successfully joined %s ", state.url.toString()));
context.openChat();
DotFloo.write(context.colabDir, state.url.toString());
if (shouldUpload) {
if (!state.readOnly) {
initialUpload(ri);
return;
}
context.statusMessage("You don't have permission to update remote files.");
}
initialManageConflicts(ri);
} catch (Throwable e) {
API.uploadCrash(context, e);
context.errorMessage("There was a critical error in the plugin" + e.toString());
context.shutdown();
}
}
});
}
void _on_get_buf(JsonObject obj) {
Gson gson = new Gson();
final GetBufResponse res = gson.fromJson(obj, (Type) GetBufResponse.class);
Buf b = state.bufs.get(res.id);
editor.queue(b, new RunLater<Buf>() {
@Override
public void run(Buf b) {
b.set(res.buf, res.md5);
b.write();
Flog.info("on get buffed. %s", b.path);
}
});
}
public void on_data(String name, JsonObject obj) {
Events event;
try {
event = Events.valueOf(name);
} catch (IllegalArgumentException e) {
Flog.log("No enum for %s", name);
return;
}
switch (event) {
case room_info:
_on_room_info(obj);
break;
case get_buf:
_on_get_buf(obj);
break;
case patch:
_on_patch(obj);
break;
case highlight:
_on_highlight(obj);
break;
case saved:
_on_saved(obj);
break;
case join:
_on_join(obj);
break;
case part:
_on_part(obj);
break;
case create_buf:
_on_create_buf(obj);
break;
case request_perms:
_on_request_perms(obj);
break;
case msg:
_on_msg(obj);
break;
case rename_buf:
_on_rename_buf(obj);
break;
case term_stdin:
_on_term_stdin(obj);
break;
case term_stdout:
_on_term_stdout(obj);
break;
case delete_buf:
_on_delete_buf(obj);
break;
case perms:
_on_perms(obj);
break;
case ping:
_on_ping(obj);
break;
case ack:
break;
default:
Flog.log("No handler for %s", name);
}
}
}