/*
* Strongback
* Copyright 2015, Strongback and individual contributors by the @authors tag.
* See the COPYRIGHT.txt in the distribution for a full listing of individual
* contributors.
*
* Licensed under the MIT License; you may not use this file except in
* compliance with the License. You may obtain a copy of the License at
* http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.strongback.tools.logdecoder;
import java.io.BufferedInputStream;
import java.io.BufferedWriter;
import java.io.DataInputStream;
import java.io.EOFException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.InvalidParameterException;
import java.util.Map;
import java.util.StringJoiner;
import org.strongback.tools.utils.FileUtils;
import org.strongback.tools.utils.Parser;
import org.strongback.tools.utils.Printer;
import org.strongback.tools.utils.Printer.Verbosity;
import org.strongback.tools.utils.Version;
/**
* Utility to convert Strongback Binary Logs into human readable csv format. Usage:
*
* The path to the input file is required and is preceded by {@code -f}. The path to the output is optional, if it is given
* it should be preced by {@code -o}, if it is not, the output will be saved in the current directory, with the same filename
* as the input, and a .csv extension. The {@code -q} and {@code -v} flags silence and increase output respectively.<br>
*
* {@code Usage: LogDecoder -f <input_file> [-o <output_file>] [-q | -v]}.
*
* <h1>Strongback Binary Log format:</h1>
* <p>
* The first three bytes are the ASCII log ({@code 6C 6F 67}). The next byte is the number of elements that have
* been logged {@code n}. The next {@code n} bytes are the number of bytes in each data point, followed by
* {@code n} repetitions of the length of the name of each data point and the name itself in ASCII bytes.
* <p>
* Now the data is recorded with respect to the number of bytes in each element.
* When Logger is stopped it will finish writing the current record followed by the terminator: {@code FF FF FF FF}.
* If the Logger was interrupted and was unable to finish writing the log, the decoder will recover as many records
* as possible. (Because the Logger is unbuffered, everything up to the point of the crash should be recovered.)
*
* <h1>Example Strongback Binary Log:</h1>
*
* <i>Demonstrates the raw binary output of the logger. New lines included only for clarity,
* and are not part of the file format</i>
* <pre> [l o g]
* [3]
* [4] [2] [2]
* [4][T i m e] [3][F o o] [3][B a r]
* [00 00 00 00] [00 52] [00 37]
* [00 00 00 0A] [04 D5] [23 AF]
* [00 00 00 14] [3F 00] [12 34]
* [FF FF FF FF]
* </pre>
*
* <h1>Output CSV format:</h1>
* <p>
* The first row contains the names of the elements as encoded into the log file, delimited by a comma. The end
* of the row is delimited by a newline character. Each following line lists the integer value of the data encoded,
* to the precision specified in the log file.
*
* <h1>Example csv file:</h1>
* <pre> Time, Foo, Bar
* 0, 82, 55,
* 10, 1237, 9135,
* 20, 16128, 4660
* </pre>
*
* <p>
* <h1>Exit Codes:</h1>
* 0 - Operation successful<br>
* 1 - Invalid arguments<br>
* 2 - Cannot open input/output file<br>
* 3 - Input file not recognized<br>
* 4 - Input/output exception<br>
* 5 - Unexpected end of file<br>
*
* @author Zach Anderson
*/
public class LogDecoder {
private static Printer printer = new Printer();
/* Overhead for command line parsing and file resolution */
public static final void main(String[] args) {
// Try to parse command line arguments
Map<String, String> opts = null;
try {
opts = Parser.parse(args, "fo", "h|f");
} catch (InvalidParameterException e) {
printer.error(e.getLocalizedMessage());
printer.print(Strings.HELP, Verbosity.ALWAYS);
System.exit(ExitCodes.INVALID_ARGUMENT);
}
assert opts != null;
if(opts.containsKey("h")) {
printer.print(Strings.HELP, Verbosity.ALWAYS);
System.exit(ExitCodes.NORMAL);
}
if(opts.containsKey("n")) {
printer.print(Strings.VERSION_HEAD, Printer.Verbosity.ALWAYS);
printer.print(Strings.VERSION, Printer.Verbosity.ALWAYS);
System.exit(ExitCodes.NORMAL);
}
printer.setVerbosity(opts.containsKey("q"), opts.containsKey("v"));
// Try to open file streams
File inPath;
File outPath;
try {
inPath = FileUtils.resolvePath(opts.get("f"));
printer.print(Strings.IN_PATH + inPath.getCanonicalPath(), Printer.Verbosity.VERBOSE);
if(!opts.containsKey("o")) {
String out = inPath.getName();
// Strips file extension
int extensionStart = out.contains(".") ? out.lastIndexOf('.') : out.length();
out = out.substring(0, extensionStart) + ".csv";
outPath = FileUtils.resolvePath(out);
} else {
outPath = FileUtils.resolvePath(opts.get("o"));
}
printer.print(Strings.OUT_PATH + outPath.getCanonicalPath(), Printer.Verbosity.VERBOSE);
// Input stream must be buffered to use mark
DataInputStream stream = new DataInputStream(new BufferedInputStream(new FileInputStream(inPath)));
BufferedWriter writer = new BufferedWriter(new FileWriter(outPath));
// Try to convert log
int exitCode = ExitCodes.NORMAL;
try {
decode(stream, writer);
printer.print(Strings.SUCCESS, Printer.Verbosity.ALWAYS);
} catch (BadFileFormatException e) {
printer.error(Strings.BAD_LOG);
exitCode = ExitCodes.BAD_LOG;
// The log is junk, delete the CSV
outPath.deleteOnExit();
} catch (EOFException e) {
printer.error(Strings.UNEXPECTED_EOF);
exitCode = ExitCodes.UNEXPECTED_EOF;
} catch (IOException e) {
printer.error(Strings.FAILED_READ);
exitCode = ExitCodes.IO_EXCEPTION;
} finally {
writer.close();
stream.close();
printer.print(Strings.OUTPUT_SAVED + outPath.getCanonicalPath(), Printer.Verbosity.ALWAYS);
System.exit(exitCode);
}
} catch (InvalidParameterException e) {
printer.error(Strings.BAD_FILEPATH);
System.exit(ExitCodes.INVALID_ARGUMENT);
} catch (FileNotFoundException e) {
printer.error(Strings.CANNOT_OPEN_FILE + e.getLocalizedMessage());
System.exit(ExitCodes.CANNOT_OPEN_FILE);
} catch (IOException e) {
printer.error(Strings.UNKNOWN_IO + e.getLocalizedMessage());
System.exit(ExitCodes.IO_EXCEPTION);
}
}
private static String readString(DataInputStream in) throws EOFException, IOException{
int len = in.readInt();
byte[] value = new byte[len];
in.read(value);
return new String(value,StandardCharsets.UTF_8);
}
/* Actually converts the log */
private static final void decode(DataInputStream in, BufferedWriter writer)
throws BadFileFormatException, EOFException, IOException {
// Verify Header
printer.print(Strings.CHECK_LOG, Printer.Verbosity.VERBOSE);
String header = readString(in);
printer.print("Found header = " + header, Printer.Verbosity.VERBOSE);
if(!"data-record".equals(header)) throw new BadFileFormatException();
printer.print(Strings.SUCCESS, Printer.Verbosity.VERBOSE);
// Get the number of channels
int numElements = in.readInt();
printer.print(Strings.ELEMENT_COUNT + numElements, Printer.Verbosity.VERBOSE);
// Get the size of each channel sample
int[] elementSizes = new int[numElements];
for(int i = 0; i< elementSizes.length; i++) {
elementSizes[i] = in.readInt();
printer.print("read channel " + i + " size = " + elementSizes[i], Printer.Verbosity.VERBOSE);
}
// Read and write the name of each channel
StringJoiner joiner = new StringJoiner(",");
for(int i = 0; i< numElements; i++) {
joiner.add(readString(in));
}
writer.write(joiner.toString());
printer.print("channel names = " + joiner, Printer.Verbosity.VERBOSE);
writer.newLine();
printer.print(Strings.READING_LOG, Printer.Verbosity.VERBOSE);
int lineCount = 0;
// Read each record
try {
in.mark(4);
while(in.readInt()!=0xFFFFFFFF) {
printer.print(Strings.READ_LINE + lineCount, Printer.Verbosity.VERBOSE);
in.reset();
for(int i = 0; i < numElements; i++) {
if (i!=0) writer.write(",");
if(elementSizes[i]==4){
int value = in.readInt();
writer.write(Integer.toString(value));
} else if(elementSizes[i]==2) {
short value = in.readShort();
writer.write(Short.toString(value));
} else {
throw new IOException("Unexpected size of data: " + elementSizes[i]);
}
}
writer.newLine();
lineCount++;
in.mark(4);
}
printer.print(Strings.SUCCESS, Printer.Verbosity.VERBOSE);
} finally {
// Always flush what we were able to decode
printer.print(Strings.FLUSH_FILES, Printer.Verbosity.VERBOSE);
writer.flush();
}
}
/* UI strings */
private static final class Strings {
private static final String LS = System.lineSeparator();
/* Version */
public static final String VERSION_HEAD = "Strongback Binary Log Decoder Utility";
public static final String VERSION = Version.versionNumber() + " compiled on " + Version.buildDate();
public static final String HELP = "usage: strongback newproject [options] -f <input_file> [-o <output_file>]"
+ LS + ""
+ LS + "Description"
+ LS + " Utility to convert Strongback Binary Log files into human readable CSV. If -o is not"
+ LS + " specified the converted log will be saved in the current directory with the same name"
+ LS + " as the binary log."
+ LS + ""
+ LS + "Options"
+ LS + " -f <input_file>"
+ LS + " The strongback binary log to convert"
+ LS + ""
+ LS + " -h"
+ LS + " Displays help information"
+ LS + ""
+ LS + " -n <project_name>"
+ LS + " The name of the new project"
+ LS + ""
+ LS + " -o"
+ LS + " The file to write the human readable csv log to. If not"
+ LS + " specified the output will be written to the current directory"
+ LS + ""
+ LS + " -p <package_name>"
+ LS + " Specifies a custom initial package for Robot.java"
+ LS + ""
+ LS + " -b"
+ LS + " Displays version information"
+ LS + ""
+ LS + " -v"
+ LS + " Displays more verbose output"
+ LS + ""
+ LS + " -q"
+ LS + " Suppressess all output"
+ LS + ""
+ LS + "Report issues at http://github.com/strongback/strongback-java"
;
/* Exit messages */
public static final String BAD_FILEPATH = "Invalid file path specified";
public static final String CANNOT_OPEN_FILE = "Can not open file: ";
public static final String BAD_LOG = "File format not recognized";
public static final String UNEXPECTED_EOF = "Unexpected end of file, did robot crash?";
public static final String UNKNOWN_IO = "An IO exception occured: ";
public static final String FAILED_READ = "The log failed to read";
public static final String OUTPUT_SAVED = "Output saved to: ";
/* Verbose messages */
public static final String IN_PATH = "Reading log at: ";
public static final String OUT_PATH = "Writing csv to: ";
public static final String CHECK_LOG = "Validating log header...";
public static final String ELEMENT_COUNT = "Found elements: ";
public static final String READING_LOG = "Reading log...";
public static final String READ_LINE = "Reading line: ";
public static final String FLUSH_FILES = "Closing resources...";
public static final String SUCCESS = "Success";
}
private static final class ExitCodes {
public static final int NORMAL = 0;
public static final int INVALID_ARGUMENT = 1;
public static final int CANNOT_OPEN_FILE = 2;
public static final int BAD_LOG = 3;
public static final int IO_EXCEPTION = 4;
public static final int UNEXPECTED_EOF = 5;
}
@SuppressWarnings("serial")
private static final class BadFileFormatException extends IOException { }
}