/**
* Copyright (C) 2010-2017 Structr GmbH
*
* This file is part of Structr <http://structr.org>.
*
* Structr is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* Structr is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Structr. If not, see <http://www.gnu.org/licenses/>.
*/
package org.structr.net.peer;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.nio.charset.Charset;
import java.security.KeyPair;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.RejectedExecutionException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.structr.net.PeerListener;
import org.structr.net.data.RemoteTransaction;
import org.structr.net.data.TimeoutException;
import org.structr.net.data.time.Clock;
import org.structr.net.data.time.PseudoTemporalEnvironment;
import org.structr.net.data.time.PseudoTime;
import org.structr.net.data.time.ToplevelTemporalEnvironment;
import org.structr.net.protocol.AbstractMessage;
import org.structr.net.protocol.Callback;
import org.structr.net.protocol.Delete;
import org.structr.net.protocol.Discovery;
import org.structr.net.protocol.Envelope;
import org.structr.net.protocol.Inventory;
import org.structr.net.protocol.Update;
import org.structr.net.repository.DefaultRepositoryObject;
import org.structr.net.repository.InternalChangeListener;
import org.structr.net.repository.Repository;
import org.structr.net.repository.RepositoryObject;
/**
* The main class of this peer-to-peer implementation. This class will
* start three individual threads. Two that handle inbound and outbound
* traffic and another one that acts on the protocol messages it receives.
*/
public final class Peer implements Runnable, Clock, InternalChangeListener {
public static final int START_PORT = 5757;
private static final Logger logger = LoggerFactory.getLogger(Peer.class.getName());
private final Queue<Envelope> outputQueue = new ConcurrentLinkedQueue<>();
private final Queue<Envelope> inputQueue = new ConcurrentLinkedQueue<>();
private final ExecutorService executorService = Executors.newCachedThreadPool();
private final Map<String, PeerInfo> peers = new ConcurrentHashMap<>();
private final Map<String, Callback> callbacks = new ConcurrentHashMap<>();
private final Charset utf8 = Charset.forName("utf-8");
private final List<PeerListener> listeners = new LinkedList<>();
private Map<String, Object> data = new HashMap<>();
private KeyPair keyPair = null;
private PrivateKey privateKey = null;
private PublicKey publicKey = null;
private ToplevelTemporalEnvironment pte = null;
private Repository repository = null;
private boolean initialized = false;
private String initialPeer = null;
private String bindAddress = null;
private DatagramSocket serverSocket = null;
private long timeOffset = 0L;
private int localPort = START_PORT;
private int sent = 0;
private int received = 0;
private boolean running = true;
private boolean verbose = false;
private int discoveryInterval = 1000;
private int discoveryIntervalStep = 1000;
private int finalDiscoveryInterval = 6000;
private int hightestTxNumber = 0;
public Peer(final KeyPair keyPair, final Repository repository) {
this(keyPair, repository, "0.0.0.0");
}
public Peer(final KeyPair keyPair, final Repository repository, final String bindAddress) {
this(keyPair, repository, bindAddress, "255.255.255.255");
}
public Peer(final KeyPair keyPair, final Repository repository, final String bindAddress, final String initialPeer) {
this.pte = new ToplevelTemporalEnvironment(this);
this.keyPair = keyPair;
this.bindAddress = bindAddress;
this.initialPeer = initialPeer;
//addListener(new PrintListener());
this.repository = repository;
// listen for internal changes in repository
repository.addInternalChangeListener(this);
}
@Override
public void run() {
long currentTime = 0L;
long lastCleanup = 0L;
long lastDiscovery = 0L;
// main loop of a peer is to listen for messages and react on it
while (running) {
try {
currentTime = System.currentTimeMillis();
// work on input queue, but interrupt for other tasks
while (!inputQueue.isEmpty() && currentTime < lastDiscovery + discoveryInterval && currentTime < lastCleanup + discoveryIntervalStep) {
currentTime = System.currentTimeMillis();
final Envelope envelope = inputQueue.poll();
if (envelope != null) {
final AbstractMessage message = envelope.getMessage();
// notify listeners
onMessage(message);
// re-broadcast to other peers
final String ackKey = message.getId() + "-ack";
// re-broadcast message if UUID was not seen before
// (this causes the "wave" effect so that all peers
// see the message, even if not connected directly)
if (getData(ackKey) == null) {
// process message
message.onMessage(this, envelope.getPeer());
// send message to other peers
broadcast(message);
setData(ackKey, true);
}
}
}
// send discovery request
if (currentTime > lastDiscovery + discoveryInterval) {
lastDiscovery = currentTime;
// insert single discovery packet into output queue
send(new PeerInfo(getPublicKey(), repository.getUuid(), initialPeer, START_PORT), new Discovery(getContentHash()));
// adjust discovery interval in steps of 10 seconds
// until it reaches 60 seconds
if (discoveryInterval < finalDiscoveryInterval) {
discoveryInterval += discoveryIntervalStep;
}
}
// remove peers that have not (re)acted for some time
if (currentTime > lastCleanup + discoveryIntervalStep) {
lastCleanup = currentTime;
// update list of peers
for (final Iterator<PeerInfo> it = peers.values().iterator(); it.hasNext();) {
final PeerInfo peer = it.next();
final long time = peer.getLastSeen() + finalDiscoveryInterval + (discoveryIntervalStep * 2);
if (currentTime > time) {
onRemovePeer(peer);
it.remove();
}
}
}
Thread.sleep(10L);
} catch (Throwable t) {
logger.warn("", t);
}
}
// shut down
executorService.shutdownNow();
}
public void start() {
if (!initialized) {
throw new IllegalStateException("Peer not initialized, did you call initializeServer?!");
}
try {
executorService.submit(new InputHandler());
executorService.submit(new OutputHandler());
executorService.submit(this);
} catch (RejectedExecutionException rex) {
logger.warn("Unable to start peer, aborting.");
executorService.shutdown();
}
}
public void initializeServer() {
boolean success = false;
while (!success && localPort < START_PORT + 10) {
try {
serverSocket = new DatagramSocket(localPort, InetAddress.getByName(bindAddress));
success = true;
} catch (IOException ioex) {
localPort++;
} catch (Throwable t) {
logger.warn("", t);
}
}
if (!success) {
System.out.println("Unable to bind to " + bindAddress + ", aborting.");
// dont start at all
running = false;
} else {
initialized = true;
}
}
public void stop() {
running = false;
serverSocket.close();
}
public String getUuid() {
return repository.getUuid();
}
public int getLocalPort() {
return localPort;
}
public long getTimeOffset() {
return timeOffset;
}
public int getTransactionNumber() {
return hightestTxNumber;
}
public void setTransactionNumber(final int transactionNumber) {
this.hightestTxNumber = transactionNumber;
}
public PseudoTemporalEnvironment getPseudoTemporalEnvironment() {
return pte;
}
public void send(final PeerInfo recipient, final AbstractMessage message) {
outputQueue.add(new Envelope(recipient, message));
}
public void onPeerDiscovery(final PeerInfo newPeer, final byte[] hash) {
if (newPeer != null && !newPeer.getUuid().equals(getUuid())) {
final boolean isNew = addPeer(newPeer);
final byte[] contentHash = getContentHash();
final boolean hasChanged = !Arrays.equals(hash, contentHash);
if (isNew || hasChanged) {
if (isNew) {
System.out.println("Peer is new, sending inventory..");
}
if (hasChanged) {
System.out.println("Peer has different content hash, sending inventory..");
System.out.println(printHash(hash) + " / " + printHash(contentHash));
}
// send inventory
for (final RepositoryObject obj : repository.getObjects()) {
log("Inventory(", obj.getUuid(), ", ", obj.getUserId(), ")");
send(newPeer, new Inventory(repository.getUuid(), obj.getUuid(), obj.getDeviceId(), obj.getLastModificationTime()));
}
}
}
}
public Collection<PeerInfo> getPeers() {
return peers.values();
}
public Repository getRepository() {
return repository;
}
public boolean isRunning() {
return running;
}
public void addListener(final PeerListener listener) {
listeners.add(listener);
}
public void removeListener(final PeerListener listener) {
listeners.remove(listener);
}
public synchronized void printInfo() {
System.out.println("#########################################");
System.out.println("Peer " + serverSocket.getLocalAddress() + ":" + localPort);
System.out.println("UUID: " + getUuid());
System.out.println("Time offset: " + timeOffset);
System.out.println(received + " messages received, " + sent + " messages sent");
System.out.println(peers.size() + " peers");
for (final PeerInfo info : peers.values()) {
System.out.println(" " + info);
}
for (final RepositoryObject obj : repository.getObjects()) {
System.out.println(" ##### " + obj.getUuid());
System.out.println(" " + obj.getType() + "(" + obj.getUserId() + "): " + obj.getProperties(pte.next()));
((DefaultRepositoryObject)obj).printHistory();
}
System.out.println(outputQueue);
System.out.flush();
}
public void setVerbose(final boolean verbose) {
this.verbose = verbose;
}
public void log(final Object... values) {
if (!verbose) {
return;
}
for(final Object obj : values) {
System.out.print(obj);
}
System.out.println();
}
public Object getData(final String key) {
return data.get(key);
}
public void setData(final String key, final Object value) {
data.put(key, value);
}
public void broadcast(final AbstractMessage message) {
// make broadcasts to self
inputQueue.add(new Envelope(new PeerInfo(getPublicKey(), repository.getUuid(), bindAddress, localPort), message));
// send message to all peers
for (final PeerInfo info : getPeers()) {
send(info, message);
}
}
public void set(final String objectId, final String key, final Object value) {
final RepositoryObject sharedObject = repository.getObject(objectId);
if (sharedObject != null) {
try (final RemoteTransaction tx = beginTx(sharedObject)) {
tx.setProperty(sharedObject, key, value);
tx.commit();
} catch (Exception tex) {
System.out.println("Failed");
}
} else {
System.out.println("No such object " + objectId);
}
}
public Object get(final String objectId, final String key) {
final RepositoryObject sharedObject = repository.getObject(objectId);
if (sharedObject != null) {
try (final RemoteTransaction tx = beginTx(sharedObject)) {
return tx.getProperty(sharedObject, key);
} catch (Exception tex) {
System.out.println("Failed");
}
} else {
System.out.println("No such object " + objectId);
}
return null;
}
public RemoteTransaction beginTx(final RepositoryObject sharedObject) throws TimeoutException {
final RemoteTransaction tx = new RemoteTransaction(this);
tx.begin(sharedObject);
return tx;
}
public void registerCallback(final String uuid, final Callback callback) {
callbacks.put(uuid, callback);
}
public void unregisterCallback(final String uuid) {
callbacks.remove(uuid);
}
public void callback(final String uuid, final AbstractMessage message) {
final Callback callback = callbacks.get(uuid);
if (callback != null) {
callback.callback(message);
callbacks.remove(uuid);
}
}
public long getCoordinatedTime() {
return System.currentTimeMillis() + timeOffset;
}
public boolean knowsPeer(final String uuid) {
return peers.containsKey(uuid);
}
public byte[] getContentHash() {
MessageDigest digest = null;
try {
digest = MessageDigest.getInstance("MD5");
} catch (NoSuchAlgorithmException nex) {
logger.warn("", nex);
}
if (digest != null) {
final long t0 = System.currentTimeMillis();
final List<RepositoryObject> objects = new LinkedList<>(repository.getObjects());
Collections.sort(objects, new UuidComparator());
for (final RepositoryObject node : objects) {
final String uuid = node.getUuid();
final String type = node.getType();
final String last = node.getLastModificationTime().toString();
digest.update(uuid.getBytes(utf8));
digest.update(type.getBytes(utf8));
digest.update(last.getBytes(utf8));
}
final long t1 = System.currentTimeMillis();
if ((t1-t0) > 100) {
System.out.println("Creation of content hash took " + (System.currentTimeMillis() - t0) + " ms!");
}
return digest.digest();
} else {
System.out.println("Cannot create hash value, algorithms not available.");
}
return new byte[0];
}
public PrivateKey getPrivateKey() {
return keyPair.getPrivate();
}
public PublicKey getPublicKey() {
return keyPair.getPublic();
}
// ---- interface Clock -----
@Override
public long getTime() {
return getCoordinatedTime();
}
// ----- interface PeerListener -----
public void onMessage(final AbstractMessage msg) {
for (final PeerListener listener : listeners) {
listener.onMessage(msg);
}
}
public void onAddPeer(final PeerInfo info) {
for (final PeerListener listener : listeners) {
listener.onAddPeer(info);
}
}
public void onRemovePeer(final PeerInfo info) {
for (final PeerListener listener : listeners) {
listener.onRemovePeer(info);
}
}
// ----- private methods -----
private synchronized boolean addPeer(final PeerInfo peer) {
final String uuid = peer.getUuid();
if (!peers.containsKey(uuid)) {
peers.put(uuid, peer);
peer.setLastSeen(System.currentTimeMillis());
onAddPeer(peer);
return true;
}
// not added
return false;
}
private synchronized void updatePeer(final String uuid, final long latency) {
final PeerInfo peer = peers.get(uuid);
if (peer != null) {
peer.setLastSeen(System.currentTimeMillis());
peer.setLatency(latency);
}
}
@Override
public void onObjectCreation(final RepositoryObject source, final Map<String, Object> data) {
log("Create(", source.getUuid(), ")");
broadcast(new Update(repository.getUuid(), source.getUuid(), source.getType(), source.getUserId(), source.getCreationTime(), source.getLastModificationTime(), data));
}
@Override
public void onObjectModification(final RepositoryObject source, final Map<String, Object> data) {
log("Update(", source.getUuid(), ")");
broadcast(new Update(repository.getUuid(), source.getUuid(), source.getType(), source.getUserId(), source.getCreationTime(), source.getLastModificationTime(), data));
}
@Override
public void onObjectDeletion(final String uuid) {
log("Delete(", uuid, ")");
broadcast(new Delete(repository.getUuid(), uuid, PseudoTime.now(this)));
}
// ----- private methods -----
private String printHash(final byte[] array) {
final StringBuilder buf = new StringBuilder();
for (final byte b : array) {
final int val = b & 0xff;
if (val < 16) {
buf.append("0");
}
buf.append(Integer.toHexString(val));
}
return buf.toString();
}
// ----- nested classes -----
private class InputHandler implements Runnable {
private final byte[] buffer = new byte[2048];
@Override
public void run() {
while (running) {
try {
final DatagramPacket packet = new DatagramPacket(buffer, 2048);
serverSocket.receive(packet);
final Envelope envelope = AbstractMessage.receive(Peer.this, packet);
if (envelope != null) {
final AbstractMessage msg = envelope.getMessage();
final long senderTimestamp = msg.getSenderTimestamp();
final long current = System.currentTimeMillis();
final long delta = senderTimestamp - current;
// adjust time offset to be in sync with other peers,
// the group's value will be the maximum of all peers
if (delta > timeOffset) {
timeOffset = delta;
}
// update last seen time
updatePeer(envelope.getPeer().getUuid(), (current + timeOffset) - senderTimestamp);
inputQueue.add(envelope);
received++;
}
} catch (Throwable t) {
logger.warn("", t);
}
}
}
}
private class OutputHandler implements Runnable {
@Override
public void run() {
while (running) {
try {
while (!outputQueue.isEmpty()) {
final Envelope envelope = outputQueue.poll();
if (envelope != null) {
final AbstractMessage message = envelope.getMessage();
final PeerInfo recipient = envelope.getPeer();
message.setSenderTimestamp(System.currentTimeMillis() + timeOffset);
message.onSend(Peer.this);
serverSocket.send(AbstractMessage.forSending(Peer.this.getUuid(), recipient, message));
sent++;
}
}
Thread.sleep(10L);
} catch (IOException ignore1) {
} catch (InterruptedException ignore2) {
} catch (Throwable t) {
logger.warn("", t);
}
}
}
}
private class UuidComparator implements Comparator<RepositoryObject> {
@Override
public int compare(final RepositoryObject o1, final RepositoryObject o2) {
return o1.getUuid().compareTo(o2.getUuid());
}
}
}