package org.opentripplanner.updater.stoptime;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.io.Writer;
import java.util.Iterator;
import java.util.List;
import java.util.zip.GZIPInputStream;
import javax.annotation.PostConstruct;
import lombok.Setter;
import org.opentripplanner.routing.trippattern.Update;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.jeromq.ZFrame;
import org.jeromq.ZMQ;
import org.jeromq.ZMsg;
/** StoptimeUpdateStreamer for CTX-encoded Dutch KV8 realtime updates over ZeroMQ */
public class KV8ZMQUpdateStreamer implements UpdateStreamer {
private static Logger LOG = LoggerFactory.getLogger(KV8ZMQUpdateStreamer.class);
private ZMQ.Context context = ZMQ.context(1);
private ZMQ.Socket subscriber = context.socket(ZMQ.SUB);
private long count = 0;
@Setter private String defaultAgencyId = "";
@Setter private String address = "tcp://node01.post.openov.nl:7817";
@Setter private static String feed = "/GOVI/KV8";
@Setter private static String messageLogFile;
@Setter private static int logFrequency = 2000;
@Setter private static String fakeInput = null; //"/home/abyrd/nl.ctx";
Writer logWriter;
Reader fakeInputReader;
@PostConstruct
public void connectToFeed() {
subscriber.connect(address);
subscriber.subscribe(feed.getBytes());
if (messageLogFile != null) {
try {
logWriter = new FileWriter(messageLogFile);
} catch (IOException e) {
LOG.warn("problem opening message log file: {}", e);
logWriter = null;
}
}
if (fakeInput != null) {
try {
fakeInputReader = new FileReader(fakeInput);
} catch (IOException e) {
LOG.warn("problem opening fake input file: {}", e);
fakeInputReader = null;
}
}
}
public List<Update> getUpdates() {
/* recvMsg blocks -- unless you call Socket.setReceiveTimeout() */
// so when timeout occurs, it does not return null, but a reference to some
// static ZMsg object?
ZMsg msg = ZMsg.recvMsg(subscriber);
if (msg == null) {
/* According to docs, null indicates that receive operation was "interrupted". */
LOG.warn("ZMQ received null message.");
return null;
}
/*
* on subscription failure, message will not be null or empty, but its content length
* will be 0 and bomb the gunzip below (or does it block forever?)
*/
List<Update> ret = null;
try {
String kv8ctx = gunzipMultifameZMsg(msg);
if (logWriter != null) {
logWriter.write(kv8ctx);
}
ret = KV8Update.fromCTX(kv8ctx);
count += 1; // if we got here there must not have been an exception
LOG.debug("decoded gzipped CTX message #{}: {}", count, msg);
if (count % logFrequency == 0) {
LOG.info("received {} KV8 messages.", count);
}
} catch (Exception e) {
LOG.error("exception while decoding (unzipping) incoming CTX message: {}", e.getMessage());
e.printStackTrace();
} finally {
msg.destroy(); // is this necessary? does ZMQ lib automatically free mem?
}
return ret;
}
private static String gunzipMultifameZMsg(ZMsg msg) throws IOException {
Iterator<ZFrame> frames = msg.iterator();
// pop off first frame, which contains "/GOVI/KV8" (the feed name) (isn't there a method for this?)
frames.next();
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
while (frames.hasNext()) {
ZFrame frame = frames.next();
byte[] frameData = frame.getData();
buffer.write(frameData);
}
if (buffer.size() == 0) {
LOG.debug("received 0-length CTX message {}", msg);
return null;
}
// chain input streams to gunzip contents of byte buffer
InputStream gzippedMessageStream = new ByteArrayInputStream(buffer.toByteArray());
InputStream messageStream = new GZIPInputStream(gzippedMessageStream);
// copy input stream back to output stream
buffer.reset();
byte[] b = new byte[4096];
for (int n; (n = messageStream.read(b)) != -1;) {
buffer.write(b, 0, n);
}
return buffer.toString();
}
}