package alma.logoot.logootengine; import java.math.BigInteger; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.LinkedList; import java.util.List; import java.util.Random; import alma.logoot.logootengine.utils.diff_match_patch; import alma.logoot.logootengine.utils.diff_match_patch.Diff; /** * Logoot algorithm implementation. * * Logoot algorithm, {@link ILogootEngine} implementation. For works this * implementation use the diff_match_patch library. * * @see http://code.google.com/p/google-diff-match-patch * * @author Adrien Bougouin adrien.bourgoin{at}gmail{dot}com * @author Adrien Drouet drizz764{at}gmail{dot}com * @author Alban Ménager alban.menager{at}gmail{dot}com * @author Alexandre Prenza prenza.a{at}gmail{dot}com * @author Ronan-Alexandre Cherrueau ronancherrueau{at}gmail{dot}com */ public class LogootEngine implements ILogootEngine { /** * The current text in model (Id Table). */ private String currentText; /** * Diff Match Patch Component. * * @see http://code.google.com/p/google-diff-match-patch */ private diff_match_patch diffEngine; /** * Logoot model. */ private List<LineId> idTable; /** * User unique id. */ private long replica; /** * User Clock. */ private int clock; /** * Logoot Constructor. */ public LogootEngine() { this.diffEngine = new diff_match_patch(); this.idTable = new ArrayList<LineId>(); this.currentText = ""; this.replica = -1; this.clock = 0; // Initilize logoot model with start and end document this.idTable.add(LineId.getDocumentStarter()); this.idTable.add(LineId.getDocumentFinisher()); } @Override public String deliver(String patch) { ArrayList<IOperation> patched = new ArrayList<IOperation>(); try { patch = patch.split("^[\\[]{2}")[1]; patch = patch.split("[\\]]{2}$")[0]; String[] splited = patch.split("[\\]],[ ][\\[]"); for (int i = 0; i < splited.length; i++) { patched.add(new Operation(splited[i])); } } catch (Exception e) { System.err.println("LogootEngine : Deserialization error."); e.printStackTrace(); } System.out.println("L'objet apres serialization : " + patched.getClass().getName() + " " + patched); // Operation o = (Operation) patched.get(0); // o.getPosition().get(o.getPosition().size()-1).getIdentifier(); // if // (o.getPosition().get(o.getPosition().size()-1).getIdentifier().equals( // id.getIdentifier())){ // System.out.println("C'est moi je ne dois pas ecrire huhu."); // return null; // } for (IOperation op : patched) deliver(op); return getCurrentText(); } @Override public String generatePatch(String text) { // Initialization by making a diff between the old text and the new one. LinkedList<Diff> diff = this.diffEngine.diff_main(getCurrentText(), text, false); setCurrentText(text); int index = 0; Collection<IOperation> patch = new ArrayList<IOperation>(); // For each difference, we need to add or delete some positions. for (Diff d : diff) { if (d.operation == alma.logoot.logootengine.utils.diff_match_patch.Operation.EQUAL) { index += d.text.length(); } else if (d.operation == alma.logoot.logootengine.utils.diff_match_patch.Operation.INSERT) { LineId p = getIdTable().get(index); LineId q = getIdTable().get(index + 1); ArrayList<LineId> idList = generateLineIdentier(p, q, d.text.length()); // Mise a jour idTable getIdTable().addAll(index + 1, idList); // Creation operations int i = 0; for (LineId lic : idList) { IOperation op = Operation.insertOperation(lic, d.text.charAt(i)); patch.add(op); i++; } index += d.text.length(); } else { // DELETE for (int i = 0; i < d.text.length(); i++) { LineId lineId = getIdTable().get(index + 1); getIdTable().remove(lineId); IOperation op = Operation.deleteOperation(lineId); patch.add(op); } } } // TODO : serialization // return serializeToJson(person); return patch.toString(); } @Override public void setId(long id) { System.out.println("LogootEngine - Reception d'un id : " + id); this.replica = id; } private String getCurrentText() { if (currentText == null) { currentText = ""; } return currentText; } private void setCurrentText(String currentText) { this.currentText = currentText; } public List<LineId> getIdTable() { return this.idTable; } /** * * @param p * premier identifiant de position * @param q * second identifiant de position * @param N * nombre d'identifiant souhaites */ public ArrayList<LineId> generateLineIdentier(LineId p, LineId q, int N) { BigInteger MAXINT = new BigInteger(Integer.MAX_VALUE + ""); ArrayList<LineId> list = new ArrayList<LineId>(); int index = 0; int interval = 0; while (interval < N) { index++; BigInteger intervalB = prefix(q, index).subtract(prefix(p, index)); intervalB = intervalB.subtract(new BigInteger("1")); if ((intervalB).compareTo(MAXINT) != -1) interval = Integer.MAX_VALUE; else interval = intervalB.intValue(); } int step; if (LogootConf.USEBOUNDARY) { step = Math.min(LogootConf.BOUNDARY, interval / N); } else { step = interval / N; } BigInteger stepB = new BigInteger(step + ""); BigInteger r = prefix(p, index); Random random = new Random(); for (int j = 0; j < N; j++) { BigInteger rand; if (step == 1) { rand = r.add(new BigInteger("1")); } else { rand = r.add(new BigInteger((random.nextInt(step - 1) + 1) + "")); } list.add(constructIdentifier(rand, p, q)); p = list.get(list.size() - 1); r = r.add(stepB); } return list; } /** * * @param r * liste d'identifiants concatenes * @param p * premier identifiant de position * @param q * second identifiant de position * @param rep * defini horloge et id de la replique pour laquelle on genere la * position * @return l'identifiant pour la replique definit par rep(id+horloge) */ public LineId constructIdentifier(BigInteger r, LineId p, LineId q) { // TODO : Ici, la fonction risque de prendre des identifiants a la fois // dans p et dans q. LineId result = new LineId(); LinkedList<Integer> prefix = prefixToList(r); int index = 0; for (int i : prefix) { Position triplet = new Position(); triplet.setDigit(i); if (index < p.size() && i == p.get(index).getDigit()) { triplet.setClock(p.get(index).getClock()); triplet.setReplica(p.get(index).getReplica()); } else if (index < q.size() && i == q.get(index).getDigit()) { triplet.setClock(q.get(index).getClock()); triplet.setReplica(q.get(index).getReplica()); } else { triplet.setClock(++this.clock); triplet.setReplica(this.replica); } index++; result.add(triplet); } return result; } /** * * @param id * Identifiant de caractere * @param n * Nombre de triplet a prendre en compte * @return les n identifiants dans la base concatenes. */ public BigInteger prefix(LineId id, int n) { String result = ""; int size = new Integer(LogootConf.BASE - 1).toString().length(); for (int i = 0; i < n; i++) { String s = ""; if (i < id.size()) { s = String.valueOf(id.get(i).getDigit()); } while (s.length() < size) s = "0" + s; result += s; } return new BigInteger(result); } private LinkedList<Integer> prefixToList(BigInteger prefix) { LinkedList<Integer> result = new LinkedList<Integer>(); String ts = String.valueOf(prefix.toString()); int size = new Integer(LogootConf.BASE - 1).toString().length(); int endIndex = ts.length(); int beginIndex = Math.max(0, endIndex - size); String cs = ts.substring(beginIndex, endIndex); result.addLast(Integer.parseInt(cs)); while (beginIndex != 0) { endIndex -= cs.length(); beginIndex = Math.max(0, endIndex - size); cs = ts.substring(beginIndex, endIndex); result.addFirst(Integer.parseInt(cs)); } return result; } private void deliver(IOperation op) { // TODO : FAIRE UNE VERIFICATION SUR LID, VERIFIER SI CE NEST PAS LE MEME // QUE CELUI DU CLIENT // ( sinon probleme dans la table des ids. ) Operation o = (Operation) op; if (o.getLineId().get(o.getLineId().size() - 1).getReplica() == replica) { return; } if (o.isIns()) { int index = -Collections.binarySearch(getIdTable(), o.getLineId()) - 1; StringBuffer sb = new StringBuffer(getCurrentText()); sb.insert(index - 1, o.getContent()); setCurrentText(sb.toString()); getIdTable().add(index, o.getLineId()); } else { int index = Collections.binarySearch(getIdTable(), o.getLineId()); if (index > 0) { StringBuffer sb = new StringBuffer(getCurrentText()); sb.deleteCharAt(index - 1); setCurrentText(sb.toString()); getIdTable().remove(index); } } } @Override public String generatePatchFromModel() { Collection<IOperation> patch = new ArrayList<IOperation>(); int i = 0; while (i < idTable.size()) { IOperation op = Operation.insertOperation(idTable.get(i), currentText.charAt(i)); patch.add(op); ++i; } return patch.toString(); } }