package nbtool.data.log; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.util.Arrays; import java.util.Vector; import com.google.protobuf.Message; import messages.InertialState; import messages.JointAngles; import nbtool.data.SExpr; import nbtool.data.calibration.CameraOffset; import nbtool.data.json.Json; import nbtool.data.json.Json.JsonValue; import nbtool.data.json.JsonArray; import nbtool.data.json.JsonObject; import nbtool.data.json.JsonParser.JsonParseException; import nbtool.images.YUYV8888Image; import nbtool.util.Debug; import nbtool.util.Debug.DebugSettings; import nbtool.util.SharedConstants; import nbtool.util.ToolSettings; import nbtool.util.Utility.Pair; import nbtool.util.test.TestBase; import nbtool.util.test.Tests; public class LogInternal extends Log { private static final DebugSettings debug = Debug.createSettings(true, true, true, Debug.WARN, null); @Override public String toString() { if (this.getReference() != null) { return String.format("%s(rid:%d)", this.getClass().getName(), this.getReference().thisID); } else { return String.format("%s(id:%d)", this.getClass().getName(), this.jvm_unique_id); } } private Pair<JsonObject, byte[]> getParts() { JsonObject top = this.topLevelDictionary.copy().asObject(); if (logClass == null || host_type == null || host_name == null || host_addr == null) { debug.warn("null field in getParts()"); } top.put(SharedConstants.LOG_TOPLEVEL_MAGIC_KEY(), ToolSettings.VERSION); top.put(SharedConstants.LOG_TOPLEVEL_LOGCLASS(), logClass); top.put(SharedConstants.LOG_TOPLEVEL_CREATED_WHEN(), createdWhen); top.put(SharedConstants.LOG_TOPLEVEL_HOST_TYPE(), host_type); top.put(SharedConstants.LOG_TOPLEVEL_HOST_NAME(), host_name); top.put(SharedConstants.LOG_TOPLEVEL_HOST_ADDR(), host_addr); int block_bytes = 0; JsonArray blockArray = new JsonArray(); top.put(SharedConstants.LOG_TOPLEVEL_BLOCKS(), blockArray); for (Block b : this.blocks) { if (b.data != null) block_bytes += b.data.length; blockArray.add(b.getFullDictionary()); } byte[] data = new byte[block_bytes]; block_bytes = 0; for (Block b : this.blocks) { if (b.data != null) { System.arraycopy(b.data, 0, data, block_bytes, b.data.length); block_bytes += b.data.length; } } debug.event("LOG PAIR: %d bytes, %s", data.length, top.serialize()); assert(block_bytes == data.length); return new Pair<JsonObject, byte[]>(top, data); } @Override public String getFullDescription() { return getParts().a.serialize(); } @Override public JsonObject getFullDictionary() { return getParts().a; } @Override public byte[] serialize() { Pair<JsonObject, byte[]> parts = getParts(); byte[] ddata = parts.a.serialize().getBytes(StandardCharsets.UTF_8); ByteArrayOutputStream baos = new ByteArrayOutputStream(ddata.length + parts.b.length + 8); DataOutputStream dos = new DataOutputStream(baos); try { dos.writeInt(ddata.length); dos.write(ddata); dos.writeInt(parts.b.length); dos.write(parts.b); assert(dos.size() == baos.size()); assert(dos.size() == (ddata.length + parts.b.length + 8)); return baos.toByteArray(); } catch (IOException e) { e.printStackTrace(); assert(false); } return null; } @Override public void writeTo(OutputStream os) throws IOException { os.write(serialize()); } @Override public int version() { return this.topLevelDictionary.get(SharedConstants.LOG_TOPLEVEL_MAGIC_KEY()).asNumber().asInt(); } @Override public boolean temporary() { return logReference == null ? true : logReference.temporary(); } @Override public void saveChangesToTempFile() throws Exception { if (logReference != null) { logReference.pushToTempFile(this); } else { throw new Exception("cannot save changes to untracked Log!"); } } @Override public void saveChangesToLoadFile() throws Exception { if (logReference != null) { logReference.pushToLoadFile(this); } else { throw new Exception("cannot save changes to untracked Log!"); } } @Override public Log deepCopy() { return LogInternal.parseFrom(this.serialize()); } private static long static_id = 0; private static final Object static_id_lock = new Object(); private static long getID() { long ret; synchronized(static_id_lock) { ret = static_id++; } return ret; } @Override protected long getUniqueID() { return getID(); } private LogInternal() {} public static Log emptyLog() { LogInternal ret = new LogInternal(); ret.createdWhen = System.currentTimeMillis(); ret.blocks = new Vector<>(); ret.topLevelDictionary = new JsonObject(); ret.logClass = SharedConstants.LogClass_Null(); ret.host_type = String.format("nbtool-v%d", ToolSettings.VERSION); ret.host_name = "computer-" + System.getProperty("user.name"); ret.host_addr = "n/a"; return ret; } public static Log explicitLog(Vector<Block> blocks, JsonObject topLevel, String logClass, long created) { Log ret = emptyLog(); ret.blocks = blocks; ret.topLevelDictionary = topLevel; ret.logClass = logClass; ret.createdWhen = created; if (topLevel.containsKey(SharedConstants.LOG_TOPLEVEL_HOST_ADDR())) { ret.host_addr = stringRemove(SharedConstants.LOG_TOPLEVEL_HOST_ADDR(), topLevel); } if (topLevel.containsKey(SharedConstants.LOG_TOPLEVEL_HOST_NAME())) { ret.host_name = stringRemove(SharedConstants.LOG_TOPLEVEL_HOST_NAME(), topLevel); } if (topLevel.containsKey(SharedConstants.LOG_TOPLEVEL_HOST_TYPE())) { ret.host_type = stringRemove(SharedConstants.LOG_TOPLEVEL_HOST_TYPE(), topLevel); } return ret; } public static Log parseFrom(byte[] bytes) { DataInputStream dis = new DataInputStream( new ByteArrayInputStream(bytes)); try { int descLen = dis.readInt(); byte[] desc = new byte[descLen]; dis.readFully(desc); int dataLen = dis.readInt(); byte[] data = new byte[dataLen]; dis.readFully(data); return parseFromParts(desc, data); } catch (IOException e) { e.printStackTrace(); assert(false); } return null; } public static Log parseFromParts(byte[] json, byte[] data) { String desc = new String(json); if (LogInternal.isLegacyDesc(desc)) { debug.warn("converting legacy log to JSON format..."); return LogInternal.parseLegacy(desc, data); } if (!desc.contains(SharedConstants.LOG_TOPLEVEL_MAGIC_KEY())) { debug.error("parsed log does not contain magic key: %s !in $s", SharedConstants.LOG_TOPLEVEL_MAGIC_KEY(), desc); throw new RuntimeException("parsed log does not contain magic key!"); } JsonObject object = null; try { object = Json.parseAndRequireEnd(desc).asObject(); } catch (JsonParseException e) { e.printStackTrace(); throw new RuntimeException(e); } int vers = object.get(SharedConstants.LOG_TOPLEVEL_MAGIC_KEY()).asNumber().asInt(); if (vers != ToolSettings.VERSION) { debug.warn("parsing log of different version to compiled tool! tool:%d log:%d", ToolSettings.VERSION, vers); } String logClass = valueRemove(SharedConstants.LOG_TOPLEVEL_LOGCLASS(), object).asString().value; long cwhen = valueRemove(SharedConstants.LOG_TOPLEVEL_CREATED_WHEN(), object).asNumber().asLong(); JsonArray blockArray = valueRemove(SharedConstants.LOG_TOPLEVEL_BLOCKS(), object).asArray(); Vector<Block> blocks = new Vector<>(); int offset = 0; for (int i = 0; i < blockArray.size(); ++i) { JsonObject bdict = blockArray.get(i).asObject(); int bytes = valueRemove(SharedConstants.LOG_BLOCK_NUM_BYTES(), bdict).asNumber().asInt(); String type = valueRemove(SharedConstants.LOG_BLOCK_TYPE(), bdict).asString().value; String from = valueRemove(SharedConstants.LOG_BLOCK_WHERE_FROM(), bdict).asString().value; long ii = valueRemove(SharedConstants.LOG_BLOCK_IMAGE_INDEX(), bdict).asNumber().asLong(); long bwm = valueRemove(SharedConstants.LOG_BLOCK_WHEN_MADE(), bdict).asNumber().asLong(); blocks.add(new Block( Arrays.copyOfRange(data, offset, offset + bytes), bdict, type, from, ii, bwm )); offset += bytes; } assert(offset == data.length); return LogInternal.explicitLog(blocks, object, logClass, cwhen); } public static Log parseFromStream(InputStream is) throws IOException { DataInputStream dis = new DataInputStream(is); int descLen = dis.readInt(); byte[] desc = new byte[descLen]; dis.readFully(desc); int dataLen = dis.readInt(); byte[] data = new byte[dataLen]; dis.readFully(data); return parseFromParts(desc, data); } private static String stringRemove(String key, JsonObject obj) { if (obj.containsKey(key)) { return obj.get(key).asString().value; } else return ""; } private static JsonValue valueRemove(String key, JsonObject obj) { if (obj.containsKey(key)) { JsonValue val = obj.get(key); obj.remove(key); return val; } else { debug.error("required key '%s' not found in Log JsonObject", key); return null; } } protected static LogReference quickParse(Path path) throws IOException { DataInputStream data = new DataInputStream(Files.newInputStream(path)); int dlen = data.readInt(); byte[] d = new byte[dlen]; data.readFully(d); String desc = new String(d); data.close(); if (LogInternal.isLegacyDesc(desc)) { debug.warn("quickParse(): converting legacy log to JSON format, cannot lazy load!"); return LogReference.referenceFromFile(path); } if (!desc.contains(SharedConstants.LOG_TOPLEVEL_MAGIC_KEY())) { debug.error("parsed log does not contain magic key: %s !in $s", SharedConstants.LOG_TOPLEVEL_MAGIC_KEY(), desc); throw new RuntimeException("parsed log does not contain magic key!"); } JsonObject object = null; try { object = Json.parseAndRequireEnd(desc).asObject(); } catch (JsonParseException e) { e.printStackTrace(); throw new RuntimeException(e); } int vers = object.get(SharedConstants.LOG_TOPLEVEL_MAGIC_KEY()).asNumber().asInt(); if (vers != ToolSettings.VERSION) { debug.warn("parsing log of different version to compiled tool! tool:%d log:%d", ToolSettings.VERSION, vers); } String logClass = valueRemove(SharedConstants.LOG_TOPLEVEL_LOGCLASS(), object).asString().value; long cwhen = valueRemove(SharedConstants.LOG_TOPLEVEL_CREATED_WHEN(), object).asNumber().asLong(); String host_addr = null; String host_name = null; String host_type = null; if (object.containsKey(SharedConstants.LOG_TOPLEVEL_HOST_ADDR())) { host_addr = stringRemove(SharedConstants.LOG_TOPLEVEL_HOST_ADDR(), object); } if (object.containsKey(SharedConstants.LOG_TOPLEVEL_HOST_NAME())) { host_name = stringRemove(SharedConstants.LOG_TOPLEVEL_HOST_NAME(), object); } if (object.containsKey(SharedConstants.LOG_TOPLEVEL_HOST_TYPE())) { host_type = stringRemove(SharedConstants.LOG_TOPLEVEL_HOST_TYPE(), object); } return new LogReference(cwhen, logClass, host_type, host_name, host_addr, desc, path); } // @Override // protected void finalize() throws Throwable { // // if (logReference != null) { // logReference.pushToTempFileNow(this); // } // //Don't pushLoad, that action must always be explicitly done by user code. // // super.finalize(); // } @Override public boolean addBlockFromProtobuf(Message message, String whereFrom, long imageIndex, long createdWhen) { String type = message.getClass().getSimpleName(); byte[] data = message.toByteArray(); blocks.add(new Block( data, new JsonObject(), type, whereFrom, imageIndex, createdWhen )); return true; } @Override public boolean addBlockFromImage(YUYV8888Image image, String whereFrom, long imageIndex, long createdWhen) { String type = SharedConstants.YUVImageType_DEFAULT(); byte[] data = Arrays.copyOf(image.data, image.data.length); blocks.add(new Block( data, new JsonObject(), type, whereFrom, imageIndex, createdWhen )); return true; } @Override public boolean addBlockFromSexpr(SExpr sexpr, String whereFrom, long imageIndex, long createdWhen) { String type = SharedConstants.SexprType_DEFAULT(); byte[] data = sexpr.serialize().getBytes(StandardCharsets.UTF_8); blocks.add(new Block( data, new JsonObject(), type, whereFrom, imageIndex, createdWhen )); return true; } @Override public boolean addBlockFromJson(JsonValue val, String whereFrom, long imageIndex, long createdWhen) { String type = SharedConstants.JsonType_DEFAULT(); byte[] data = val.serialize().getBytes(StandardCharsets.UTF_8); blocks.add(new Block( data, new JsonObject(), type, whereFrom, imageIndex, createdWhen )); return true; } @Override public boolean addBlockFromLog(Log log) { String type = SharedConstants.LogType_DEFAULT(); byte[] data = log.serialize(); blocks.add(new Block( data, new JsonObject(), type, "n/a", 0, 0 )); return true; } @Override public Block find(String type) { for (Block b : blocks) { if (b.type.equals(type)) return b; } return null; } private static boolean isLegacyDesc(String desc) { char sc = desc.trim().charAt(0); switch(sc) { case '{': return false; case '(': return true; default: debug.error("pre-version 6 description: %s", desc); throw new RuntimeException("LogInternal cannot parse pre-version 6 log!"); } } private static Log parseLegacy(String desc, byte[] data) { SExprLog slog = new SExprLog(desc, data); // debug.printf("%s", desc); // debug.printf("%s %s %s", slog.primaryType(), slog.primaryFrom(), slog.primaryEncoding()); if (slog.contentCount() < 3 || !slog.primaryType().equals("YUVImage") || !slog.madeWhere().equals("tripoint") || !slog.primaryEncoding().equals("[Y8(U8/V8)]") ) { debug.error("cannot parse non-tripoint legacy log, returning null"); return null; } debug.info("%s", slog.tree().print()); JsonObject imDict = Json.object(); imDict.put(SharedConstants.LOG_BLOCK_IMAGE_WIDTH_PIXELS(), slog.primaryWidth()); imDict.put(SharedConstants.LOG_BLOCK_IMAGE_HEIGHT_PIXELS(), slog.primaryHeight()); Vector<Block> blocks = new Vector<>(); blocks.add(new Block( slog.bytesForContentItem(0), imDict, SharedConstants.YUVImageType_DEFAULT(), slog.primaryFrom(), slog.primaryImgIndex(), // slog.primaryTime() 0L )); blocks.add(new Block( slog.bytesForContentItem(1), new JsonObject(), "InertialState", slog.primaryFrom(), 0, 0 )); blocks.add(new Block( slog.bytesForContentItem(2), new JsonObject(), "JointAngles", slog.primaryFrom(), 0, 0 )); CameraOffset co = null; boolean calParamsFound = false; Vector<SExpr>[] found = null; found = slog.tree().recursiveFindAll("camera_TOP"); for (Vector<SExpr> vec : found) { if (vec.lastElement().count() == 3) { debug.info("found %s", vec.lastElement()); calParamsFound = true; co = CameraOffset.fromLisp(vec.lastElement()); break; } } if (!calParamsFound) { found = slog.tree().recursiveFindAll("camera_BOT"); for (Vector<SExpr> vec : found) { if (vec.lastElement().count() == 3) { debug.info("found %s", vec.lastElement()); calParamsFound = true; co = CameraOffset.fromLisp(vec.lastElement()); break; } } } assert(calParamsFound); JsonObject top = Json.object(); top.put(SharedConstants.LOG_TOPLEVEL_MAGIC_KEY(), 7); top.put("OriginalCameraOffsets", co.toObject()); Log ret = Log.explicitLog(blocks, top, SharedConstants.LogClass_Tripoint(), 0L); ret.host_addr = slog.tree().firstValueOf(SExprLog.LOG_FROM_ADDR_S).value(); ret.host_name = ret.host_addr; ret.host_type = slog.tree().firstValueOf(SExprLog.LOG_HOST_TYPE_S).value(); return ret; } public static void _NBL_ADD_TESTS_() { Tests.add("data.log", new TestBase("basic tests"){ @Override public boolean testBody() throws Exception { Log test1 = Log.explicitLog(new Vector<Block>(), new JsonObject(), "classy", 0L); // debug.info("%s", test1.getFullDescription()); byte[] testBytes = test1.serialize(); Log test2 = Log.parseFrom(testBytes); // debug.info("\t\tjvm_unique..."); assert (test2.jvm_unique_id == test1.jvm_unique_id + 1); // debug.info("\t\tparts equal..."); Pair<JsonObject, byte[]> t1P = ((LogInternal) test1).getParts(); Pair<JsonObject, byte[]> t2P = ((LogInternal) test2).getParts(); // debug.info("%s", t1P.a.serialize()); assert(t1P.a.congruent(t2P.a)); assert (Arrays.equals(t1P.b, t2P.b)); return true; } }, new TestBase("parseFrom_bytes") { @Override public boolean testBody() throws Exception { Log outer = Log.emptyLog(); Log a1 = Log.explicitLog(new Vector<Block>(), new JsonObject(), "none", 0); Log a2 = Log.explicitLog(new Vector<Block>(), new JsonObject(), "thingy", 0); SExpr sexpr = SExpr.deserializeFrom("(none, none, ())"); outer.addBlockFromLog(a1); outer.addBlockFromLog(a2); outer.addBlockFromSexpr(sexpr, "testing", 0, 0); outer.blocks.add(new Block(null, new JsonObject(), "none", "", 0, 0 )); byte[] bytes = outer.serialize(); Log parsed = Log.parseFrom(bytes); assert(parsed.blocks.size() == 4); Block b1 = parsed.blocks.get(0); Block b2 = parsed.blocks.get(2); Block b3 = parsed.blocks.get(3); Debug.print("%d bytes", b3.data.length); assert(b3.data.length == 0); assert(b1.type.equals(SharedConstants.LogType_DEFAULT())); assert(b2.parseAsSExpr().serialize().equals(sexpr.serialize())); Log none = Log.emptyLog(); Log np = Log.parseFrom(none.serialize()); assert(np.host_type.equals(String.format("nbtool-v%d", ToolSettings.VERSION))); return true; } }, new TestBase("parseFrom_stream") { @Override public boolean testBody() throws Exception { SExpr sexpr = SExpr.deserializeFrom("(none, none, ())"); InputStream is = TestBase.resourceAtClass(this, "testLog1"); Log pstream = Log.parseFromStream(is); assert(pstream.blocks.size() == 4); Block b22 = pstream.blocks.get(2); assert(b22.parseAsSExpr().serialize().equals(sexpr.serialize())); Debug.info("parseFromStream OK..."); return true; } }, new TestBase("legacy") { @Override public boolean testBody() throws Exception { Log test = Log.parseFromStream(TestBase.resourceAtClass(this, "testLog2")); YUYV8888Image img = test.blocks.get(0).parseAsYUVImage(); Message m1 = test.blocks.get(1).parseAsProtobufOfClass(InertialState.class); Message m2 = test.blocks.get(2).parseAsProtobufOfClass(JointAngles.class); debug.info("json: %s", test.getFullDescription()); return true; } }, new TestBase("temporary_and_pushing") { @Override public boolean testBody() throws Exception { Log test = Log.emptyLog(); assert(test.logReference == null); assert(test.temporary()); return true; } } ); } }