blob: 7d25511e1fe02ca07583d9b7d5cb67cb0743b544 [file] [log] [blame]
// Copyright 2016 The Vanadium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package examples.baku.io.permissions.synchronization;
import android.util.Log;
import com.google.firebase.database.ChildEventListener;
import com.google.firebase.database.DataSnapshot;
import com.google.firebase.database.DatabaseError;
import com.google.firebase.database.DatabaseException;
import com.google.firebase.database.DatabaseReference;
import com.google.firebase.database.GenericTypeIndicator;
import com.google.firebase.database.MutableData;
import com.google.firebase.database.Transaction;
import com.google.firebase.database.ValueEventListener;
import org.bitbucket.cowwoc.diffmatchpatch.DiffMatchPatch;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.UUID;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import examples.baku.io.permissions.PermissionManager;
/**
* Created by phamilton on 6/24/16.
*/
public class SyncText {
static final String KEY_CURRENT = "current";
static final String KEY_TEXT = "value";
static final String KEY_VERSION = "version";
static final String KEY_PATCHES = "patches";
static final String KEY_SUBSCRIBERS = "subscribers";
static final String KEY_DIFFS = "diffs";
private final GenericTypeIndicator<ArrayList<SyncTextDiff>> diffListType = new GenericTypeIndicator<ArrayList<SyncTextDiff>>() {
};
private LinkedList<SyncTextDiff> diffs = new LinkedList<>();
private int ver;
private BlockingQueue<SyncTextPatch> mPatchQueue;
private DiffMatchPatch diffMatchPatch = new DiffMatchPatch();
private DatabaseReference mSyncRef;
private DatabaseReference mPatchesRef;
private DatabaseReference mOutputRef;
private PatchConsumer mPatchConsumer;
private OnTextChangeListener mOnTextChangeListener;
private String mInstanceId;
private String mLocalSource;
private int mPermissions;
public SyncText(String local, int permissions, DatabaseReference reference, DatabaseReference output) {
if (reference == null) throw new IllegalArgumentException("null reference");
mLocalSource = local;
mPermissions = permissions;
mSyncRef = reference;
mOutputRef = output;
mInstanceId = UUID.randomUUID().toString();
mPatchQueue = new LinkedBlockingQueue<>();
mPatchConsumer = new PatchConsumer(mPatchQueue);
new Thread(mPatchConsumer).start();
link();
}
public LinkedList<SyncTextDiff> getDiffs() {
return diffs;
}
public int getPermissions() {
return mPermissions;
}
public void setPermissions(int mPermissions) {
this.mPermissions = mPermissions;
if ((mPermissions & PermissionManager.FLAG_WRITE) == PermissionManager.FLAG_WRITE) {
acceptSuggestions(mLocalSource);
}
}
public static String getFinalText(LinkedList<SyncTextDiff> diffs) {
String result = "";
for (SyncTextDiff diff : diffs) {
if (diff.operation == SyncTextDiff.EQUAL) {
result += diff.getText();
}
}
return result;
}
public void setOnTextChangeListener(OnTextChangeListener onTextChangeListener) {
this.mOnTextChangeListener = onTextChangeListener;
}
public int update(String newText) {
if (mPatchesRef == null) {
throw new RuntimeException("database connection hasn't been initialized");
}
LinkedList<DiffMatchPatch.Patch> patches = diffMatchPatch.patchMake(fromDiffs(this.diffs), newText);
if (patches.size() > 0) {
String patchString = diffMatchPatch.patchToText(patches);
SyncTextPatch patch = new SyncTextPatch();
patch.setVer(ver + 1);
patch.setPatch(patchString);
if (mLocalSource != null) {
patch.setSource(mLocalSource);
}
patch.setPermissions(mPermissions);
mPatchesRef.push().setValue(patch);
return patch.getVer();
}
return -1;
}
//TODO: this method currently waits for server confirmation to notify listeners. Ideally, it should notify immediately and revert on failure
private void updateCurrent(final int ver, final LinkedList<SyncTextDiff> diffs) {
final String text = getFinalText(diffs);
this.ver = ver;
this.diffs = diffs;
mSyncRef.child(KEY_CURRENT).removeEventListener(mCurrentValueListener);
mSyncRef.child(KEY_CURRENT).runTransaction(new Transaction.Handler() {
@Override
public Transaction.Result doTransaction(MutableData currentData) {
if (currentData.getValue() == null) {
currentData.child(KEY_TEXT).setValue(text);
currentData.child(KEY_VERSION).setValue(ver);
currentData.child(KEY_DIFFS).setValue(diffs);
} else {
int latest = currentData.child(KEY_VERSION).getValue(Integer.class);
if (latest > ver) {
return Transaction.abort();
}
currentData.child(KEY_TEXT).setValue(text);
currentData.child(KEY_VERSION).setValue(ver);
currentData.child(KEY_DIFFS).setValue(diffs);
}
return Transaction.success(currentData);
}
@Override
public void onComplete(DatabaseError databaseError, boolean success, DataSnapshot dataSnapshot) {
if (success) {
notifyListeners(diffs, ver);
}
mSyncRef.child(KEY_CURRENT).addValueEventListener(mCurrentValueListener);
}
});
}
private void notifyListeners(LinkedList<SyncTextDiff> diffs, int ver) {
String text = getFinalText(diffs);
if (mOnTextChangeListener != null) {
mOnTextChangeListener.onTextChange(text, diffs, ver);
}
if (mOutputRef != null) { //pass successful change to output location
mOutputRef.setValue(text);
}
}
public void link() {
mSyncRef.child(KEY_SUBSCRIBERS).child(mInstanceId).setValue(0);
mPatchesRef = mSyncRef.child(KEY_PATCHES);
mSyncRef.child(KEY_CURRENT).addListenerForSingleValueEvent(new ValueEventListener() {
@Override
public void onDataChange(DataSnapshot dataSnapshot) {
if (dataSnapshot.exists()) {
if (dataSnapshot.hasChild(KEY_DIFFS)) {
diffs = new LinkedList<SyncTextDiff>(dataSnapshot.child(KEY_DIFFS).getValue(diffListType));
}
ver = dataSnapshot.child(KEY_VERSION).getValue(Integer.class);
} else { //version 0, empty string
updateCurrent(0, new LinkedList<>(diffs));
}
notifyListeners(diffs, ver);
// mPatchesRef.orderByChild(KEY_VERSION).startAt(ver).addChildEventListener(mPatchListener);
mPatchesRef.addChildEventListener(mPatchListener);
mSyncRef.child(KEY_CURRENT).addValueEventListener(mCurrentValueListener);
}
@Override
public void onCancelled(DatabaseError databaseError) {
}
});
}
private ValueEventListener mCurrentValueListener = new ValueEventListener() {
@Override
public void onDataChange(DataSnapshot dataSnapshot) {
if (dataSnapshot.exists()) {
int version = dataSnapshot.child(KEY_VERSION).getValue(Integer.class);
if (version > ver && dataSnapshot.hasChild(KEY_DIFFS)) {
ver = version;
diffs = new LinkedList<SyncTextDiff>(dataSnapshot.child(KEY_DIFFS).getValue(diffListType));
notifyListeners(diffs, ver);
}
}
}
@Override
public void onCancelled(DatabaseError databaseError) {
}
};
private ChildEventListener mPatchListener = new ChildEventListener() {
@Override
public void onChildAdded(DataSnapshot dataSnapshot, String s) {
try {
SyncTextPatch patch = dataSnapshot.getValue(SyncTextPatch.class);
if (patch != null) {
mPatchQueue.add(patch);
}
} catch (DatabaseException e) {
e.printStackTrace();
}
dataSnapshot.getRef().removeValue();
}
@Override
public void onChildChanged(DataSnapshot dataSnapshot, String s) {
}
@Override
public void onChildRemoved(DataSnapshot dataSnapshot) {
}
@Override
public void onChildMoved(DataSnapshot dataSnapshot, String s) {
}
@Override
public void onCancelled(DatabaseError databaseError) {
}
};
public void unlink() {
mSyncRef.child(KEY_PATCHES).removeEventListener(mPatchListener);
mSyncRef.child(KEY_SUBSCRIBERS).child(mInstanceId).removeValue();
}
public interface OnTextChangeListener {
void onTextChange(String finalText, LinkedList<SyncTextDiff> diffs, int ver);
}
private class PatchConsumer implements Runnable {
private final BlockingQueue<SyncTextPatch> queue;
PatchConsumer(BlockingQueue q) {
queue = q;
}
public void run() {
try {
while (true) {
consume(queue.take());
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
void consume(SyncTextPatch patch) {
processPatch(patch);
}
}
private static String fromDiffs(List<SyncTextDiff> diffs) {
String result = "";
for (SyncTextDiff diff : diffs) {
result += diff.getText();
}
return result;
}
boolean hasWrite(SyncTextPatch patch) {
return (patch.getPermissions() & PermissionManager.FLAG_WRITE) == PermissionManager.FLAG_WRITE;
}
//TODO: bug when duplicate letter patterns in the text. The diff algorithm doesn't take source into account.
//TODO: this method doesn't handle delete operations on diffs with different sources (e.g. deleting a suggestion from another source), these operations are currently ignored
void processPatch(SyncTextPatch patch) {
int v = patch.getVer();
if (this.ver >= v) { //ignore patches for previous versions
return;
}
String previous = fromDiffs(this.diffs);
String source = patch.getSource();
LinkedList<DiffMatchPatch.Patch> patches = new LinkedList<>(diffMatchPatch.patchFromText(patch.getPatch()));
Object[] patchResults = diffMatchPatch.patchApply(patches, previous);
if (patchResults == null) { //return if failed to apply patch
return;
}
String patched = (String) patchResults[0];
LinkedList<DiffMatchPatch.Diff> diffs = diffMatchPatch.diffMain(previous, patched);
LinkedList<SyncTextDiff> result = new LinkedList<>();
ListIterator<SyncTextDiff> previousIterator = new LinkedList<>(this.diffs).listIterator();
SyncTextDiff previousDiff = null;
SyncTextDiff last = null;
int length;
for (DiffMatchPatch.Diff current : diffs) {
int operation = current.operation.ordinal();
String value = current.text;
if (previousDiff == null && previousIterator.hasNext()) {
previousDiff = previousIterator.next();
}
switch (operation) {
case SyncTextDiff.EQUAL:
length = value.length();
while (previousDiff.length() < length) {
result.add(previousDiff);
length -= previousDiff.length();
previousDiff = previousIterator.next();
}
if (previousDiff.length() == length) {
result.add(previousDiff);
if (previousIterator.hasNext()) {
previousDiff = previousIterator.next();
}
} else {
SyncTextDiff splitDiff = previousDiff.truncate(length);
result.add(previousDiff);
previousDiff = splitDiff;
}
break;
case SyncTextDiff.INSERT:
last = result.peekLast();
if (last != null && last.compatible(operation, source) && !hasWrite(patch)) {
last.text += value;
} else if (hasWrite(patch)) {
result.add(new SyncTextDiff(current.text, SyncTextDiff.EQUAL, source, patch.getPermissions()));
} else {
result.add(new SyncTextDiff(current.text, operation, source, patch.getPermissions()));
}
break;
case SyncTextDiff.DELETE:
length = value.length();
while (previousDiff.length() <= length) {
if (!hasWrite(patch) && (!source.equals(previousDiff.source) || previousDiff.operation != SyncTextDiff.INSERT)) {
previousDiff.setSource(source);
previousDiff.setOperation(operation);
result.add(previousDiff);
}
length -= previousDiff.length();
if (previousIterator.hasNext()) {
previousDiff = previousIterator.next();
}
}
if (length > 0) {
if (!hasWrite(patch) && (!source.equals(previousDiff.source) || previousDiff.operation != SyncTextDiff.INSERT)) {
SyncTextDiff splitDiff = previousDiff.truncate(length);
previousDiff.setSource(source);
previousDiff.setOperation(operation);
result.add(previousDiff);
previousDiff = splitDiff;
} else {
previousDiff.text = previousDiff.text.substring(0, length);
}
}
break;
}
}
//merge compatible diffs
reduceDiffs(result);
updateCurrent(v, result);
}
static void reduceDiffs(LinkedList<SyncTextDiff> diffs) {
if (!diffs.isEmpty()) {
Iterator<SyncTextDiff> iterator = diffs.iterator();
SyncTextDiff neighbor = iterator.next();
while (iterator.hasNext()) {
SyncTextDiff diff = iterator.next();
if (neighbor.compatible(diff)) {
neighbor.text += diff.text;
iterator.remove();
} else {
neighbor = diff;
}
}
}
}
public void acceptSuggestions() {
acceptSuggestions(mLocalSource);
}
public void acceptSuggestions(String source) {
LinkedList<SyncTextDiff> result = new LinkedList<>(diffs);
for (Iterator<SyncTextDiff> iterator = result.iterator(); iterator.hasNext(); ) {
SyncTextDiff diff = iterator.next();
if (diff.source.equals(source)) {
switch (diff.operation) {
case SyncTextDiff.DELETE:
iterator.remove();
break;
default:
diff.operation = SyncTextDiff.EQUAL;
break;
}
}
}
updateCurrent(ver + 1, result);
}
public void rejectSuggestions() {
rejectSuggestions(mLocalSource);
}
public void rejectSuggestions(String source) {
LinkedList<SyncTextDiff> result = new LinkedList<>(diffs);
for (Iterator<SyncTextDiff> iterator = result.iterator(); iterator.hasNext(); ) {
SyncTextDiff diff = iterator.next();
if (diff.source.equals(source)) {
switch (diff.operation) {
case SyncTextDiff.DELETE:
diff.operation = SyncTextDiff.EQUAL;
break;
case SyncTextDiff.INSERT:
iterator.remove();
break;
}
}
}
updateCurrent(ver + 1, result);
}
}