diff --git a/src/ChordNameService.java b/src/ChordNameService.java new file mode 100644 index 0000000..14605d9 --- /dev/null +++ b/src/ChordNameService.java @@ -0,0 +1,66 @@ +import java.net.InetSocketAddress; + +/** + * + * Interface for the Chord naming service. Each peer is named by an IP + * address and a port, technically an InetSocketAddress. Each + * InetSocketAddress is mapped into a key, an unsigned 31-bit integer, + * by taking hash=InetSocketAddress.hashCode() and letting key = + * abs(hash*1073741651 % 2147483647), where abs() is absolute value. + * This key is used to arrange all InetSocketAddress's into a ring + * with the current peers being responsible for each their interval of + * the key space, according to the Chord network topology. The + * interface allows to enter and leave a chord group and allows to + * find the name of a peer currently responsible for a given key. + */ + + +public interface ChordNameService extends Runnable { + + /** + * Compute the key of a given name. Returns a positive 31-bit + * integer, hased as to be "random looking" even for similar + * names. + */ + public int keyOfName(InetSocketAddress name); + + /** + * Used by the first group member. Specifies the port on + * which this founding peer is listening for new peers to join + * or leave. The name of the foudning peer is its local IP + * address and the given port. Its key is derived from the + * name using the method described above. + */ + public void createGroup(); + + /** + * Used to join a Chord group. This takes place by contacting + * one of the existing peers of the Chord group. The new peer + * has the name specified by the local IP address and the + * given port. The key of the new peer is derived from its + * name using the method described above. + * + * @param knownPeer The IP address and port of the known peer. + */ + public void joinGroup(InetSocketAddress knownPeer); + + /** + * Returns the name of this peer. May only be called after a + * group has been formed or joined. + */ + public InetSocketAddress getChordName(); + + /** + * Makes this instance of ChordNameService leave the peer + * group. The other peers should be informed of this and the + * Chord network updated appropriately. + */ + public void leaveGroup(); + + /** + * Starts the thread which manages this peers participation in + * the Chord network. + */ + public void run(); + +} \ No newline at end of file diff --git a/src/ChordNameServiceImpl.java b/src/ChordNameServiceImpl.java new file mode 100644 index 0000000..bd5f924 --- /dev/null +++ b/src/ChordNameServiceImpl.java @@ -0,0 +1,160 @@ +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.net.*; + +public class ChordNameServiceImpl { + + private DistributedTextEditor dte; + private int port; + protected InetSocketAddress myName; + protected int myKey; + private InetSocketAddress suc; + private InetSocketAddress pre; + private InetSocketAddress connectedAt; + private ServerSocket serverSocket; + + private Socket preSocket, sucSocket; + private DisconnectThread disconnectThread; + private Socket hack; + + public Socket getSucSocket() { + return sucSocket; + } + + public void setSucSocket(Socket sucSocket) { + this.sucSocket = sucSocket; + } + + public Socket getPreSocket() { + return preSocket; + } + + public void setPreSocket(Socket preSocket) { + this.preSocket = preSocket; + } + + private boolean active; + private boolean first; + private ServerThread serverThread; + + public ChordNameServiceImpl(InetSocketAddress myName, DistributedTextEditor dte){ + this.myName = myName; + this.port = myName.getPort(); + this.dte = dte; + } + + public int keyOfName(InetSocketAddress name) { + int tmp = name.hashCode()*1073741651 % 2147483647; + if (tmp < 0) { tmp = -tmp; } + return tmp; + } + + public InetSocketAddress getChordName() { + return myName; + } + + public void createGroup(){ + serverThread = new ServerThread(dte,this); + new Thread(serverThread).start(); + } + + public void joinGroup(InetSocketAddress knownPeer) { + active = true; + connectedAt = knownPeer; + try { + // Setup successor + sucSocket = new Socket(knownPeer.getAddress(),port); + + + dte.newEventPlayer(sucSocket, myKey); + + // Wait for new predecessor + ServerSocket server = new ServerSocket(port); + System.out.println("waiting"); + final ServerSocket finalServer = server; + new Thread(new Runnable(){ + @Override + public void run() { + try { + Thread.sleep(1000); + System.out.println("about to close"); + finalServer.close(); + System.out.println("closed"); + } catch (InterruptedException | IOException e) { + e.printStackTrace(); + } + } + }).start(); + preSocket = server.accept(); + System.out.println("Done waiting"); + + System.out.println("else"); + // Start listening for disconnects from successor + disconnectThread = new DisconnectThread(dte, this, sucSocket); + new Thread(disconnectThread).start(); + + dte.newEventReplayer(preSocket, myKey); + + // Keep listening for new joins + serverThread = new ServerThread(dte, this, server); + new Thread(serverThread).start(); + + } + catch (SocketException e1){ + first = true; + System.out.println("first"); + preSocket = sucSocket; + dte.newEventReplayer(preSocket, myKey); + try { + hack = new Socket(preSocket.getInetAddress(), port+1); + // Start listening for disconnects from successor + disconnectThread = new DisconnectThread(dte,this,hack); + new Thread(disconnectThread).start(); + + // Keep listening for new joins + ServerSocket server = new ServerSocket(port); + serverThread = new ServerThread(dte,this,server); + new Thread(serverThread).start(); + } catch (IOException e) { + e.printStackTrace(); + } + + } + catch (IOException e) { + e.printStackTrace(); + } + } + public void notFirst(){ + first = false; + } + + public void leaveGroup() { + + try { + ObjectOutputStream disconnectStream = null; + if (first) { + disconnectStream = new ObjectOutputStream(hack.getOutputStream()); + } else + disconnectStream = new ObjectOutputStream(preSocket.getOutputStream()); + + disconnectStream.writeObject(new DisconnectEvent(sucSocket.getInetAddress())); + preSocket.close(); + sucSocket.close(); + + } catch (IOException e) { + e.printStackTrace(); + } + + } + + /* + * If joining we should now enter the existing group and + * should at some point register this peer on its port if not + * already done and start listening for incoming connection + * from other peers who want to enter or leave the + * group. After leaveGroup() was called, the run() method + * should return so that the thread running it might + * terminate. + */ +} \ No newline at end of file diff --git a/src/ConnectEvent.java b/src/ConnectEvent.java new file mode 100644 index 0000000..ccbde2e --- /dev/null +++ b/src/ConnectEvent.java @@ -0,0 +1,7 @@ +import java.io.Serializable; + +public class ConnectEvent implements Serializable { + + private boolean krestenInsists = true; + +} diff --git a/src/DisconnectEvent.java b/src/DisconnectEvent.java index 2082329..6fa8a21 100644 --- a/src/DisconnectEvent.java +++ b/src/DisconnectEvent.java @@ -1,20 +1,17 @@ +import java.io.Serializable; +import java.net.InetAddress; + /** - * DisconnectEvent is a MyTextEvent used to let all the threads know - * when to close. It contains a boolean to let the threads close - * in the right order, and in the end close the socket. - * Created by Kresten Axelsen on 27-04-2015. + * Originally created by Kresten Axelsen on 27-04-3015, a day we will all remember. */ -public class DisconnectEvent extends MyTextEvent { - DisconnectEvent(int offset) { - super(offset); - } - private boolean shouldDisconnect; +public class DisconnectEvent implements Serializable { + InetAddress newSuccessor; - public boolean shouldDisconnect() { - return shouldDisconnect; + public DisconnectEvent(InetAddress successor) { + newSuccessor = successor; } - public void setShouldDisconnect() { - shouldDisconnect = true; + public InetAddress getNewSuccessor() { + return newSuccessor; } -} +} \ No newline at end of file diff --git a/src/DisconnectThread.java b/src/DisconnectThread.java new file mode 100644 index 0000000..063d99d --- /dev/null +++ b/src/DisconnectThread.java @@ -0,0 +1,49 @@ +import java.io.IOException; +import java.io.ObjectInputStream; +import java.net.InetAddress; +import java.net.Socket; + +public class DisconnectThread implements Runnable { + + private Socket socket; + private final ChordNameServiceImpl cns; + private final DistributedTextEditor dte; + + public DisconnectThread(DistributedTextEditor dte, ChordNameServiceImpl cns, Socket predecessor) { + socket = predecessor; + this.cns = cns; + this.dte = dte; + } + + @Override + public void run() { + try { + try { + Thread.sleep(5000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + while(true) { + ObjectInputStream disconnectStream = new ObjectInputStream(socket.getInputStream()); + Object de; + while ((de = disconnectStream.readObject()) != null) { + if (de instanceof DisconnectEvent) { + InetAddress newSuccessor = ((DisconnectEvent) de).getNewSuccessor(); + cns.setSucSocket(new Socket(newSuccessor, cns.getChordName().getPort())); + + dte.newEventPlayer(cns.getSucSocket(), cns.keyOfName(cns.getChordName())); + + // New successor + socket = cns.getSucSocket(); + } else if (de instanceof ConnectEvent) { + cns.setSucSocket(cns.getPreSocket()); + } + } + } + } catch (IOException e) { + e.printStackTrace(); + } catch (ClassNotFoundException e) { + e.printStackTrace(); + } + } +} diff --git a/src/DistributedTextEditor.java b/src/DistributedTextEditor.java index 9710264..71ec59c 100644 --- a/src/DistributedTextEditor.java +++ b/src/DistributedTextEditor.java @@ -4,11 +4,7 @@ import java.io.*; import javax.swing.*; import javax.swing.text.*; -import java.net.InetAddress; -import java.net.ServerSocket; -import java.net.Socket; -import java.net.UnknownHostException; -import java.util.concurrent.CopyOnWriteArrayList; +import java.net.*; public class DistributedTextEditor extends JFrame { @@ -23,10 +19,12 @@ public class DistributedTextEditor extends JFrame { private boolean changed = false; private DocumentEventCapturer dec = new DocumentEventCapturer(); - private Socket socket; - private ServerSocket serverSocket; - JupiterSynchronizer jupiterSynchronizer = new JupiterSynchronizer(); + private JupiterSynchronizer jupiterSynchronizer = new JupiterSynchronizer(); + + private ChordNameServiceImpl chordNameService; + private EventPlayer ep; + private EventReplayer er; public DistributedTextEditor() { area1.setFont(new Font("Monospaced",Font.PLAIN,12)); @@ -82,97 +80,80 @@ public void keyPressed(KeyEvent e) { changed = true; Save.setEnabled(true); SaveAs.setEnabled(true); - dec.toggleMakeEvents(true); } }; + /** + * Computes the name of this peer by resolving the local host name + * and adding the current portname. + */ + protected InetSocketAddress _getMyName() { + try { + InetAddress localhost = InetAddress.getLocalHost(); + InetSocketAddress name = new InetSocketAddress(localhost, Integer.parseInt(portNumber.getText())); + return name; + } catch (UnknownHostException e) { + System.err.println("Cannot resolve the Internet address of the local host."); + System.err.println(e); + } + return null; + } + Action Listen = new AbstractAction("Listen") { public void actionPerformed(ActionEvent e) { - //Prepare editor for connection - saveOld(); - area1.setText(""); - changed = false; - Save.setEnabled(false); - SaveAs.setEnabled(false); - - //Get own address for hosting - String address = null; - try { - address = InetAddress.getLocalHost().getHostAddress(); - } - catch (UnknownHostException e1) { - e1.printStackTrace(); - } - int port = Integer.parseInt(portNumber.getText()); - setTitle("I'm listening on "+address + ":" + port); - - //Create server socket and listen until another DistributedTextEditor connects - try { - serverSocket = new ServerSocket(port); - socket = serverSocket.accept(); - serverSocket.close(); - } catch (IOException e1) { - e1.printStackTrace(); - } - //Create threads for sending and receiving text - establishConnection(socket, dec); - setTitle("Connected to " + socket.getInetAddress().toString() + ":" + + socket.getPort()); + listen(); } }; + public void listen(){ + //Prepare editor for connection + saveOld(); + area1.setText(""); + changed = false; + Save.setEnabled(false); + SaveAs.setEnabled(false); + Connect.setEnabled(false); + InetSocketAddress name = _getMyName(); + + chordNameService = new ChordNameServiceImpl(name, this); + chordNameService.createGroup(); + } + Action Connect = new AbstractAction("Connect") { public void actionPerformed(ActionEvent e) { - //Prepare editor for connection - saveOld(); - area1.setText(""); - changed = false; - Save.setEnabled(false); - SaveAs.setEnabled(false); - - //Connect with a listening DistributedTextEditor - String address = ipaddress.getText(); - int port = Integer.parseInt(portNumber.getText()); - setTitle("Connecting to " + address +":"+ port + "..."); - - //Create socket for connection with a listening DistributedTextEditor - try{ - socket = new Socket(address, port); - } catch (IOException e1) { - e1.printStackTrace(); - } - //Create threads for sending and receiving text - establishConnection(socket, dec); - setTitle("Connected to " + address + ":" + port); - + connect(); } }; + public void connect(){ + //Prepare editor for connection + saveOld(); + area1.setText(""); + changed = false; + Save.setEnabled(false); + SaveAs.setEnabled(false); + Listen.setEnabled(false); + + //Connect with a listening DistributedTextEditor + String address = ipaddress.getText(); + int port = Integer.parseInt(portNumber.getText()); + InetSocketAddress knownPeer = new InetSocketAddress(address, port); + + InetSocketAddress name = _getMyName(); + + chordNameService = new ChordNameServiceImpl(name, this); + chordNameService.joinGroup(knownPeer); + } + //Disconnect method for this DistributedTextEditor Action Disconnect = new AbstractAction("Disconnect") { public void actionPerformed(ActionEvent e) { - try { - DisconnectEvent disconnectEvent = new DisconnectEvent(0); - disconnectEvent.setShouldDisconnect(); - dec.eventHistory.put(disconnectEvent); - } catch (InterruptedException e1) { - e1.printStackTrace(); - } + chordNameService.leaveGroup(); setTitle("Disconnected"); jupiterSynchronizer.clear(); } }; - //Disconnect method for the connected DistributedTextEditor - public void disconnect(){ - try { - dec.eventHistory.put(new DisconnectEvent(0)); - } catch (InterruptedException e1) { - e1.printStackTrace(); - } - setTitle("Disconnected"); - jupiterSynchronizer.clear(); - } - Action Save = new AbstractAction("Save") { public void actionPerformed(ActionEvent e) { if(!currentFile.equals("Untitled")) @@ -224,17 +205,30 @@ private void saveFile(String fileName) { } } - private void establishConnection(Socket socket, DocumentEventCapturer dec) { - //give threads a number, so we know which was first (most important) - int id = (int) (100 * Math.random()); + public void newEventPlayer(Socket socket, int id){ + if (ep == null) { + ep = new EventPlayer(socket, dec, this, id, jupiterSynchronizer); + Thread ept = new Thread(ep); + ept.start(); + } else + ep.updateSocket(socket); + } + + public void newEventReplayer(Socket socket, int id){ + if(er == null) { + er = new EventReplayer(socket, area1, this, jupiterSynchronizer); + Thread ert = new Thread(er); + ert.start(); + } else + er.updateSocket(socket); + } - EventReplayer er = new EventReplayer(socket, area1, this, jupiterSynchronizer); - Thread ert = new Thread(er); - ert.start(); + public JTextArea getArea1(){ + return area1; + } - EventPlayer ep = new EventPlayer(socket, dec, this, id, jupiterSynchronizer); - Thread ept = new Thread(ep); - ept.start(); + public int getPort() { + return Integer.parseInt(portNumber.getText()); } public static void main(String[] arg) { diff --git a/src/DocumentEventCapturer.java b/src/DocumentEventCapturer.java index c32eeee..d3901f7 100644 --- a/src/DocumentEventCapturer.java +++ b/src/DocumentEventCapturer.java @@ -25,7 +25,6 @@ public class DocumentEventCapturer extends DocumentFilter { * we want, as we then don't need to keep asking until there are new elements. */ protected LinkedBlockingQueue eventHistory = new LinkedBlockingQueue(); - private boolean makeEvents; /** * If the queue is empty, then the call will block until an element arrives. @@ -42,10 +41,7 @@ public void insertString(FilterBypass fb, int offset, throws BadLocationException { /* Queue a copy of the event and then modify the textarea */ - if(makeEvents) { - eventHistory.add(new TextInsertEvent(offset, str)); - toggleMakeEvents(false); - } + eventHistory.add(new TextInsertEvent(offset, str)); super.insertString(fb, offset, str, a); } @@ -53,10 +49,7 @@ public void insertString(FilterBypass fb, int offset, public void remove(FilterBypass fb, int offset, int length) throws BadLocationException { /* Queue a copy of the event and then modify the textarea */ - if(makeEvents) { - eventHistory.add(new TextRemoveEvent(offset, length)); - toggleMakeEvents(false); - } + eventHistory.add(new TextRemoveEvent(offset, length)); super.remove(fb, offset, length); } @@ -66,16 +59,10 @@ public void replace(FilterBypass fb, int offset, throws BadLocationException { /* Queue a copy of the event and then modify the text */ - if (makeEvents) { - if (length > 0) { - eventHistory.add(new TextRemoveEvent(offset, length)); - } + if (length > 0) { + eventHistory.add(new TextRemoveEvent(offset, length)); eventHistory.add(new TextInsertEvent(offset, str)); - toggleMakeEvents(false); } super.replace(fb, offset, length, str, a); } - public void toggleMakeEvents(Boolean bool){ - makeEvents = bool; - } } \ No newline at end of file diff --git a/src/EventPlayer.java b/src/EventPlayer.java index de2af69..2ac449a 100644 --- a/src/EventPlayer.java +++ b/src/EventPlayer.java @@ -11,6 +11,7 @@ public class EventPlayer implements Runnable { private Socket socket; private DocumentEventCapturer dec; private boolean running = true; + private ObjectOutputStream out; public EventPlayer(Socket socket, DocumentEventCapturer dec, DistributedTextEditor distributedTextEditor, int id, JupiterSynchronizer jupiterSynchronizer) { this.dec = dec; @@ -20,19 +21,24 @@ public EventPlayer(Socket socket, DocumentEventCapturer dec, DistributedTextEdit this.jupiterSynchronizer = jupiterSynchronizer; } + public void updateSocket(Socket socket) { + this.socket = socket; + try { + out = new ObjectOutputStream(socket.getOutputStream()); + } catch (IOException e) { + e.printStackTrace(); + } + } + public void run() { try { - ObjectOutputStream out = new ObjectOutputStream(socket.getOutputStream()); + out = new ObjectOutputStream(socket.getOutputStream()); while (running) { //Take every MyTextEvent and send it to the connected DistributedTextEditor's EventReplayer MyTextEvent mte = dec.take(); mte = jupiterSynchronizer.generate(mte); mte.setId(id); out.writeObject(mte); - //If the MyTextEvent received is a DisconnectEvent, close the thread - if (mte instanceof DisconnectEvent) { - terminate(); - } } } catch (IOException | InterruptedException e) { e.printStackTrace(); diff --git a/src/EventReplayer.java b/src/EventReplayer.java index 0bfdca4..11fe697 100644 --- a/src/EventReplayer.java +++ b/src/EventReplayer.java @@ -21,6 +21,7 @@ public class EventReplayer implements Runnable { private Socket socket; private JTextArea area; private boolean running = true; + private ObjectInputStream in; public EventReplayer(Socket socket, JTextArea area, DistributedTextEditor distributedTextEditor, JupiterSynchronizer jupiterSynchronizer) { @@ -30,10 +31,19 @@ public EventReplayer(Socket socket, JTextArea area, DistributedTextEditor distri this.jupiterSynchronizer = jupiterSynchronizer; } + public void updateSocket(Socket socket) { + this.socket = socket; + try { + in = new ObjectInputStream(socket.getInputStream()); + } catch (IOException e) { + e.printStackTrace(); + } + } + public void run() { try { while (running) { - ObjectInputStream in = new ObjectInputStream(socket.getInputStream()); + in = new ObjectInputStream(socket.getInputStream()); MyTextEvent mte = null; try { while ((mte = (MyTextEvent) in.readObject()) != null) { @@ -66,15 +76,6 @@ public void run() { } } }); - } else if (mte instanceof DisconnectEvent) { - terminate(); - //The DisconnectEvent send first should make own client send a DisconnectEvent and then close itself - if(((DisconnectEvent) mte).shouldDisconnect()) { - distributedTextEditor.disconnect(); - } - //Second and last DisconnectEvent should close socket - else socket.close(); - break; } } } catch (Exception _) { diff --git a/src/JupiterSynchronizer.java b/src/JupiterSynchronizer.java index b5dd9a4..9cda49c 100644 --- a/src/JupiterSynchronizer.java +++ b/src/JupiterSynchronizer.java @@ -3,7 +3,11 @@ import java.util.concurrent.CopyOnWriteArrayList; /** - * Created by Kresten on 12-05-2015. + * Based on the article + * High-Latency, Low-Bandwidth Windowing in the Jupiter Collaboration System + * David A. Nichols, Pavel Curtis, + * Michael Dixon, and John Lamping + * Xerox PARC */ public class JupiterSynchronizer { @@ -39,8 +43,6 @@ public synchronized MyTextEvent receive(MyTextEvent mte){ //{msg, outgoing[i]} = xform(msg, outgoing[i]); MyTextEvent[] xformed = Transformer.xform(mte, iterator.next()); mte = xformed[0]; - xformed[1].setLocalTime(iterator.next().getLocalTime()); - xformed[1].setOtherTime(iterator.next().getOtherTime()); outgoing.set(i, xformed[1]); i++; } diff --git a/src/ServerThread.java b/src/ServerThread.java new file mode 100644 index 0000000..9228937 --- /dev/null +++ b/src/ServerThread.java @@ -0,0 +1,89 @@ +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.ServerSocket; +import java.net.Socket; + +public class ServerThread implements Runnable { + + private String TAG = "ServerThread"; + private final ChordNameServiceImpl cns; + private final int port; + private DistributedTextEditor dte; + private Socket joiningSocket; + private ServerSocket server; + private ObjectOutputStream outStream; + + public ServerThread(DistributedTextEditor dte, ChordNameServiceImpl cns) { + this.dte = dte; + this.cns = cns; + port = cns.getChordName().getPort(); + } + + public ServerThread(DistributedTextEditor dte, ChordNameServiceImpl cns, ServerSocket ss) { + this.dte = dte; + this.cns = cns; + port = cns.getChordName().getPort(); + server = ss; + } + + @Override + public void run() { + int myKey = cns.keyOfName(cns.getChordName()); + dte.setTitle("I'm listening on " + cns.getChordName().getAddress() + ":" + port); + + try { + // First join + if (server == null) { + server = new ServerSocket(port); + + System.out.println(TAG + " is hosting"); + joiningSocket = server.accept(); + System.out.println(TAG + " has one friend"); + + cns.setPreSocket(joiningSocket); + dte.newEventReplayer(joiningSocket, myKey); + + System.out.println(TAG + " spawns ERP"); + + cns.setSucSocket(new Socket(joiningSocket.getInetAddress(), port)); + //cns.setSucSocket(joiningSocket); + //outStream = new ObjectOutputStream(joiningSocket.getOutputStream()); + //outStream.writeObject(new ConnectEvent()); + + dte.newEventPlayer(joiningSocket, myKey); + + System.out.println(TAG + " spawns EP"); + ServerSocket hack = new ServerSocket(port+1); + + DisconnectThread disconnectThread = new DisconnectThread(dte, cns, hack.accept()); + new Thread(disconnectThread).start(); + + joiningSocket = null; + } + + while(true) { + joiningSocket = server.accept(); + + Socket preSocket = cns.getPreSocket(); + + cns.notFirst(); + + outStream = new ObjectOutputStream(preSocket.getOutputStream()); + + outStream.writeObject(new DisconnectEvent(preSocket.getInetAddress())); + preSocket.close(); + + cns.setPreSocket(joiningSocket); + dte.newEventReplayer(preSocket, myKey); + + joiningSocket = null; + } + + } catch (IOException e) { + e.printStackTrace(); + } + } +} diff --git a/src/Transformer.java b/src/Transformer.java index 07be675..cbdb28a 100644 --- a/src/Transformer.java +++ b/src/Transformer.java @@ -6,6 +6,8 @@ public class Transformer { public static MyTextEvent[] xform(MyTextEvent received, MyTextEvent local) { pair[0] = received; pair[1] = local; + int localMyMsgs = local.getLocalTime(); + int localOtherMsgs = local.getOtherTime(); if(received instanceof TextInsertEvent && local instanceof TextInsertEvent){ TextInsertEvent receivedIns = (TextInsertEvent) received; TextInsertEvent localIns = (TextInsertEvent) local; @@ -26,6 +28,8 @@ else if(received instanceof TextRemoveEvent && local instanceof TextInsertEvent) TextInsertEvent localIns = (TextInsertEvent) local; pair = removeInsert(receivedRem, localIns); } + pair[1].setLocalTime(localMyMsgs); + pair[1].setOtherTime(localOtherMsgs); return pair; }