zklogtool/src/main/java/com/zklogtool/util/PropertiesReader.java
public class PropertiesReader {

    File propertiesFile;
    Properties prop;

    /**
     *
     * @param propertiesFile Zookeeper configuration file.
     * @throws IOException Thrown if there is a problem with reading
     * <code>propertiesFile</code>.
     */
    public PropertiesReader(File propertiesFile) throws IOException {

        this.propertiesFile = propertiesFile;

        prop = new Properties();
        prop.load(new FileInputStream(propertiesFile));

    }

    /**
     *
     * @return Directory where transaction log files are stored.
     */
    public String getTransactionLogDir() {

        if (prop.containsKey("dataLogDir")) {
            return prop.getProperty("dataLogDir") + "/version-2";
        } else if (prop.containsKey("dataDir")) {
            return prop.getProperty("dataDir") + "/version-2";
        } else {
            return null;
        }

    }

    /**
     *
     * @return Directory where snapshot files are stored.
     */
    public String getSnapshotDir() {

        if (prop.containsKey("dataDir")) {
            return prop.getProperty("dataDir") + "/version-2";
        } else {
            return null;
        }

    }

    public File getPropertiesFile() {
        return propertiesFile;
    }

}
zklogtool/src/main/java/com/zklogtool/data/DataDirTransactionLogFileList.java
public class DataDirTransactionLogFileList implements TransactionLogFileList {

    private File dataDir;
    private DataDirHelper dataDirHelper;

    /**
     *
     * @param dataDir Directory where transaction log files are stored.
     */
    public DataDirTransactionLogFileList(File dataDir) {
        this.dataDir = dataDir;
        dataDirHelper = new DataDirHelper(dataDir, null);
    }

    /**
     * Returns chronologically first transaction log file from directory
     * injected in constructor. That is file that has lowest zxid in filename.
     *
     * @return Chronologically first transaction log file from directory
     * injected in constructor.
     * @throws NoFileException Thrown if there are no transaction log files at
     * all in directory injected in constructor.
     */
    @Override
    public File getFirstTransactionLog() throws NoFileException {

        List<File> transactionLogs = dataDirHelper.getSortedLogList();

        if (!transactionLogs.isEmpty()) {
            return transactionLogs.get(0);
        } else {
            throw new NoFileException("No transaction log found in " + dataDir.getAbsolutePath());
        }

    }

    /**
     * Returns transaction log file that chronologically succeeds
     * <code>previousTransactionLog</code>. That is file with next lowest zxid
     * number in name.
     *
     * @param previousTransactionLog Transaction log file that chronologically
     * precedes transaction log that is to be returned.
     * @return Transaction log file that chronologically succeeds
     * <code>previousTransactionLog</code>.
     * @throws NoFileException Thrown if there is no transaction log file with
     * higher zxid number in directory injected in constructor.
     */
    @Override
    public File getNextTransactionLog(File previousTransactionLog) throws NoFileException {

        List<File> transactionLogs = dataDirHelper.getSortedLogList();

        for (int i = 0; i < transactionLogs.size() - 1; i++) {

            if (previousTransactionLog.getAbsolutePath().contentEquals(transactionLogs.get(i).getAbsolutePath())) {
                return transactionLogs.get(i + 1);
            }

        }

        throw new NoFileException("No transaction log found in " + dataDir.getAbsolutePath() 
                + "that succeeds" + previousTransactionLog.toString());

    }

}
zklogtool/src/main/java/com/zklogtool/data/TransactionLogFileList.java
public interface TransactionLogFileList {

    /**
     *
     * @return First transaction log file to be processed.
     * @throws NoFileException Thrown if file is not (yet) accessible.
     */
    File getFirstTransactionLog() throws NoFileException;

    /**
     *
     * @param previousTransactionLog Transaction log file that precedes
     * file that is to be returned. 
     * @return Transaction log file that succeeds
     * <code>previousTransactionLog</code> transaction log file.
     * @throws NoFileException Thrown if file is not (yet) accessible.
     */
    File getNextTransactionLog(File previousTransactionLog) throws NoFileException;

}
zklogtool/src/main/java/com/zklogtool/printer/TransactionPrinter.java
public class TransactionPrinter {

    StringBuilder sb;
    DataDecoder dd;

    int indentation;

    /**
     *
     * @param sb <code>StringBuilder</code> to which information is be appended
     * @param dd Decoder used to convert transaction data byte array to
     * <code>String</code>. Transaction types that hold data are create and
     * setData.
     */
    public TransactionPrinter(StringBuilder sb, DataDecoder dd) {
        this.sb = sb;
        this.dd = dd;

        indentation = 0;
    }

    public int getIndentation() {
        return indentation;
    }

    /**
     * Indentation if number of tab characters appended in front of actual data.
     *
     * @param indentation Positive integer that represents indentation.
     */
    public void setIndentation(int indentation) {
        this.indentation = indentation;
    }

    private void println(String s) {
        for (int i = 0; i < indentation; i++) {
            sb.append("\t");
        }
        sb.append(s).append(lineSeparator());
    }

    private void printOperation(String operation) {
        println("Operation:\t" + operation);
    }

    private void printPath(String path) {
        println("Path:\t\t" + path);
    }

    private void printEphermal(boolean ephermal) {
        println("Ephermal:\t" + ephermal);
    }

    private void printParentCVersion(int parentCVersion) {
        println("ParentCVersion:\t" + parentCVersion);
    }

    private void printVersion(int version) {
        println("Version:\t" + version);
    }

    private void printTimeout(int timeOut) {
        println("Timeout:\t" + timeOut);
    }

    private void printError(int error) {
        println("Error:\t\t" + error);
    }

    private void printHeader(TxnHeader txnHeader) {
        println("Zxid:\t\t" + Util.longToHexString(txnHeader.getZxid()));
        println("Cxid:\t\t" + Util.longToHexString(txnHeader.getCxid()));
        println("Client id:\t" + Util.longToHexString(txnHeader.getClientId()));
        println("Time:\t\t" + new Date(txnHeader.getTime()));
    }

    private void printRecord(Record r, int type) {

        switch (type) {

            case OpCode.create:

                //check if you should use CreateTxnV0 instead
                CreateTxn createTxn = (CreateTxn) r;
                printOperation("create");
                printPath(createTxn.getPath());
                printEphermal(createTxn.getEphemeral());
                printParentCVersion(createTxn.getParentCVersion());
                printACL(createTxn.getAcl());
                printData(createTxn.getData());
                break;

            case OpCode.delete:

                DeleteTxn deleteTxn = (DeleteTxn) r;
                printOperation("delete");
                printPath(deleteTxn.getPath());
                break;

            case OpCode.setData:

                SetDataTxn setDataTxn = (SetDataTxn) r;
                printOperation("setData");
                printPath(setDataTxn.getPath());
                printData(setDataTxn.getData());
                printVersion(setDataTxn.getVersion());
                break;

            case OpCode.setACL:
                SetACLTxn setACLTxn = (SetACLTxn) r;
                printOperation("setACL");
                printPath(setACLTxn.getPath());
                printACL(setACLTxn.getAcl());
                printVersion(setACLTxn.getVersion());
                break;

            case OpCode.check:
                CheckVersionTxn checkVersionTxn = (CheckVersionTxn) r;
                printOperation("check");
                printPath(checkVersionTxn.getPath());
                printVersion(checkVersionTxn.getVersion());
                break;

            case OpCode.multi:
                MultiTxn multiTxn = (MultiTxn) r;
                printOperation("multi");
                indentation++;

                for (Txn txn : multiTxn.getTxns()) {
                    ByteBuffer bb = wrap(txn.getData());
                    Record record = null;
                    switch (txn.getType()) {
                        case OpCode.create:
                            record = new CreateTxn();
                            break;
                        case OpCode.delete:
                            record = new DeleteTxn();
                            break;
                        case OpCode.setData:
                            record = new SetDataTxn();
                            break;
                        case OpCode.error:
                            record = new ErrorTxn();
                            break;
                        case OpCode.check:
                            record = new CheckVersionTxn();
                            break;
                        default:
                            break;
                    }

                    try {

                        byteBuffer2Record(bb, record);
                        printRecord(record, txn.getType());

                    } catch (IOException ex) {
                        //this should never happen
                    }
                }

                indentation--;

                break;

            //create and close session use the same txn
            case OpCode.createSession:
                CreateSessionTxn createSessionTxn = (CreateSessionTxn) r;
                printOperation("createSession");
                printTimeout(createSessionTxn.getTimeOut());
                break;

            case OpCode.closeSession:
                CreateSessionTxn closeSessionTxn = (CreateSessionTxn) r;
                printOperation("closeSession");
                break;

            case OpCode.error:
                ErrorTxn errorTxn = (ErrorTxn) r;
                printOperation("error");
                printError(errorTxn.getErr());
                break;

            //there is also SetMaxChildrenTxn but not sure if needed
        }

    }

    /**
     *
     * @param t <code>Transaction</code> to print information from.
     */
    public void print(Transaction t) {

        TxnHeader h = t.getTxnHeader();
        Record r = t.getTxnRecord();

        printHeader(h);
        printRecord(r, h.getType());

    }

    private void printData(byte[] b) {

        if (b != null) {
            println("Data:\t\t" + dd.decode(b));
        } else {
            println("Data:\t\tnull");
        }

    }

    private void printACL(List<ACL> acl) {

        println("ACL:");

        indentation += 2;

        for (ACL a : acl) {

            String perms = getACLString(a.getPerms());
            String id = a.getId().getId();
            String scheme = a.getId().getScheme();

            println(scheme + ":" + id + "  " + perms);
        }

        indentation -= 2;

    }

}
zklogtool/src/main/java/com/zklogtool/monitor/TransactionMonitor.java
public class TransactionMonitor {

    boolean running = false;
    TransactionLog transactionLog;
    TransactionIterator iterator;

    List<TransactionListener> listeners = new ArrayList<>();

    Thread monitorThread = new Thread() {

        public void run() {

            TransactionState lastState = null;

            while (running) {

                try {

                    while (iterator.nextTransactionState() == TransactionState.OK) {

                        notifyOnTransaction(iterator.next());

                    }

                    TransactionState n = iterator.nextTransactionState();

                    if (n == TransactionState.INCOMPLETE && n != lastState) {
                        notifyOnPartialTransaction();
                    } else if (n == TransactionState.CORRUPTION && n != lastState) {
                        notifyOnCorruption();
                    }

                    lastState = n;

                    sleep(50);

                } catch (InterruptedException ex) {
                    //boring
                }
            }
        }

    };

    /**
     *
     * @param transactionLog <code>TransactionLog</code> to monitor for new
     * <code>Transaction</code>s.
     */
    public TransactionMonitor(final TransactionLog transactionLog) {

        this.transactionLog = transactionLog;

        monitorThread.setName("TransactionMonitor thread");

    }

    /**
     * Start <code>TransactionMonitor</code> in a way that it notifies listeners
     * about all already written transactions and than continues monitoring.
     *
     */
    public void startAtFirstTransaction() {

        if (!running) {

            iterator = transactionLog.iterator();

            running = true;

            monitorThread.start();

        }

    }

    /**
     * Start <code>TransactionMonitor</code> in a way that it notifies listeners
     * only about transactions that were not written before monitoring started.
     *
     */
    public void startAtLastTransaction() {

        if (!running) {

            iterator = transactionLog.iterator();

            while (iterator.nextTransactionState() == TransactionState.OK) {

                iterator.next();

            }

            running = true;

            monitorThread.start();
        }

    }

    /**
     * Stops monitoring thread.
     *
     */
    public void stop() {

        running = false;

    }

    /**
     *
     * @param l <code>TransactionListener</code> to be added to listeners list.
     */
    public void addListener(TransactionListener l) {

        listeners.add(l);

    }

    /**
     *
     * @param l <code>TransactionListener</code> to be removed from listeners
     * list.
     */
    public void removeListener(TransactionListener l) {

        listeners.remove(l);

    }

    private void notifyOnTransaction(Transaction t) {

        //listener could change transaction, not safe, maybe send a copy
        for (TransactionListener l : listeners) {
            l.onTransaction(t);
        }
    }

    private void notifyOnPartialTransaction() {

        for (TransactionListener l : listeners) {
            l.onPartialTransaction();
        }

    }

    private void notifyOnCorruption() {

        for (TransactionListener l : listeners) {
            l.onCorruption();
        }

    }

}
zklogtool/src/main/java/com/zklogtool/printer/DataDecoder.java
public interface DataDecoder {
   
    /**
     *
     * @param data
     * @return
     */
    public String decode(byte[] data);
    
}
zklogtool/src/main/java/com/zklogtool/data/Transaction.java
public class Transaction {

    private TxnHeader txnHeader;
    private Record txnRecord;

    public Transaction(TxnHeader txnHeader, Record txnRecord) {
        super();
        this.txnHeader = txnHeader;
        this.txnRecord = txnRecord;
    }

    public TxnHeader getTxnHeader() {
        return txnHeader;
    }

    public void setTxnHeader(TxnHeader txnHeader) {
        this.txnHeader = txnHeader;
    }

    public Record getTxnRecord() {
        return txnRecord;
    }

    public void setTxnRecord(Record txnRecord) {
        this.txnRecord = txnRecord;
    }

}
zklogtool/src/main/java/com/zklogtool/data/TransactionLog.java
public class TransactionLog implements Iterable<Transaction> {

    final TransactionLogFileList transactionLogList;
    final TransactionLogReaderFactory factory;

    /**
     * Constructs <code>TransactionLog</code> that provides iterator over
     * <code>Transactions</code> in one transaction log file
     * <code>transactionLogFile</code>.
     *
     * @param transactionLogFile Transaction log file to iterate over.
     * @param factory <code>TransactionLogReaderFactory</code> that provides
     * <code>TransactionLogFileReader</code> to be used.
     */
    public TransactionLog(final File transactionLogFile, TransactionLogReaderFactory factory) {

        this.transactionLogList = new TransactionLogFileList() {

            @Override
            public File getFirstTransactionLog() throws NoFileException {
                return transactionLogFile;
            }

            @Override
            public File getNextTransactionLog(File previousTransactionLog) throws NoFileException {
                throw new NoFileException("");
            }

        };

        this.factory = factory;

    }

    /**
     * Constructs <code>TransactionLog</code> that provides iterator over
     * <code>Transactions</code> written in multiple transaction log files.
     * Transaction log files are provided by
     * <code>transactionLogFileList</code>.
     *
     * @param transactionLogFileList Provides transaction log files to iterate
     * over.
     * @param factory <code>TransactionLogReaderFactory</code> that provides
     * <code>TransactionLogFileReader</code> to be used.
     */
    public TransactionLog(TransactionLogFileList transactionLogFileList, TransactionLogReaderFactory factory) {

        this.transactionLogList = transactionLogFileList;
        this.factory = factory;

    }

    @Override
    public TransactionIterator iterator() {

        return new TransactionLogIterator(transactionLogList, factory);
    }

    /**
     * Implements <code>TransactionIterator</code>. Remove operation is not
     * supported.
     *
     */
    public class TransactionLogIterator implements TransactionIterator {

        private TransactionLogFileReader reader;
        private TransactionLogFileList transactionLogList;

        private Transaction t;
        private TransactionState s;

        private TransactionLogIterator(TransactionLogFileList transactionLogFileList, TransactionLogReaderFactory factory) {

            this.transactionLogList = transactionLogFileList;

            loadNextTransaction();

        }

        @Override
        public boolean hasNext() {

            if (nextTransactionState() == TransactionState.OK) {
                return true;
            } else {
                return false;
            }

        }

        @Override
        public Transaction next() {

            if (hasNext()) {

                Transaction temp = t;

                loadNextTransaction();

                return temp;

            } else {
                throw new NoSuchElementException("No such element");
            }

        }

        @Override
        public void remove() {
            throw new UnsupportedOperationException("Not supported");
        }

        /**
         *
         * @return TransactionState of the next <code>Transaction</code> in
         * iteration.
         */
        @Override
        public TransactionState nextTransactionState() {

            if (s != TransactionState.OK) {
                loadNextTransaction();
            }

            return s;
        }

        private void loadNextTransaction() {

            try {

                if (reader == null) {

                    reader = factory.getReader(transactionLogList.getFirstTransactionLog());

                }

                t = reader.getNextTransaction();

                if (t != null) {
                    s = TransactionState.OK;
                } else {
                    s = TransactionState.EMPTY;

                    try {

                        reader = factory.getReader(transactionLogList.getNextTransactionLog(reader.getTransactionLogFile()));

                    } catch (NoFileException ex) {
                        // this is ok, there is no next file so default behaviour is to stick with current file
                    }
                }

            } catch (IncompleteTransactionException ex) {

                s = TransactionState.INCOMPLETE;

            } catch (CRCValidationException ex) {

                s = TransactionState.CORRUPTION;

            } catch (IOException ex) {

                s = TransactionState.CORRUPTION;

            } catch (NoFileException ex) {

                s = TransactionState.EMPTY;

            }

        }

    }

}
zklogtool/src/main/java/com/zklogtool/data/TransactionIterator.java
public interface TransactionIterator extends Iterator<Transaction> {

    /**
     *
     * @return <code>TransactionState</code> of the next
     * <code>Transaction</code>.
     */
    public TransactionState nextTransactionState();

}
zklogtool/src/main/java/com/zklogtool/data/TransactionState.java
public enum TransactionState {

    /**
     * Transaction is completely written and CRC is successfully validated.
     */
    OK,
    /**
     * Transaction is not written at all.
     */
    EMPTY,
    /**
     * Part of transaction is written.
     */
    INCOMPLETE,
    /**
     * Part or entire transaction is written but CRC validation has failed.
     */
    CORRUPTION
}
zklogtool/src/main/java/com/zklogtool/reader/TransactionLogReaderFactory.java
public class TransactionLogReaderFactory {

    public TransactionLogFileReader getReader(File transactionLog) throws FileNotFoundException, IOException {

        return new TransactionLogFileReader(transactionLog);

    }

}
zklogtool/src/main/java/com/zklogtool/monitor/TransactionListener.java
public interface TransactionListener {

    /**
     * Called when transaction is entirely written to transaction log.
     *
     * @param t <code>Transaction</code> written to transaction log file.
     */
    void onTransaction(Transaction t);

    /**
     * Called when transaction is not entirely written to transaction log file.
     *
     */
    void onPartialTransaction();

    /**
     * Called when there is corruption detected while reading transaction log
     * file.
     *
     */
    void onCorruption();

}
zklogtool/src/main/java/com/zklogtool/printer/UnicodeDecoder.java
public class UnicodeDecoder implements DataDecoder {

    @Override
    public String decode(byte[] data) {

        //maybe should check charset before 
        return new String(data);
    }

}


Question: import java.io.File;
import java.io.IOException;
import static java.lang.System.exit;
import com.beust.jcommander.Parameter;
import com.beust.jcommander.Parameters;
import com.zklogtool.data.DataDirTransactionLogFileList;
import com.zklogtool.data.Transaction;
import com.zklogtool.data.TransactionIterator;
import com.zklogtool.data.TransactionLog;
import com.zklogtool.data.TransactionLogFileList;
import com.zklogtool.data.TransactionState;
import com.zklogtool.monitor.TransactionListener;
import com.zklogtool.monitor.TransactionMonitor;
import com.zklogtool.printer.DataDecoder;
import com.zklogtool.printer.TransactionPrinter;
import com.zklogtool.printer.UnicodeDecoder;
import com.zklogtool.reader.TransactionLogReaderFactory;
import com.zklogtool.util.PropertiesReader;
/* 
 * Copyright 2014 Alen Caljkusic.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * 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 com.zklogtool.cli;


/**
 * Represents <b>log</b> command and holds logic for <b>log</b> command
 * execution. It is also used by jCommander for command parameters.
 *
 */
@Parameters(commandDescription = "Display transaction log entries")
public class CommandLog {

    /**
     * If true zklogtool will monitor transaction log directory and printout new
     * transactions as they are written to disk. Tool will never exit on it's
     * own.
     *
     */
    @Parameter(names = Arguments.FOLLOW, description = "Output appended data as the transactions are written to logs")
    public boolean follow = false;

    /**
     * When <code>Arguments.FOLLOW</code> option is used user may choose to
     * printout only transactions that were written to disk after zklogtool
     * started.
     *
     */
    @Parameter(names = Arguments.START_WITH_LAST_TRANSACTION, description = "Start printout from last written transaction. Can only be used with follow option")
    public boolean startWithLastTransaction = false;

    /**
     * Directory that holds Zookeeper transaction log files.
     *
     */
    @Parameter(names = Arguments.DATA_LOG_DIR, description = "Zookeeper log direcory path")
    public String dataLogDir;

    /**
     * Single Zookeeper transaction log file.
     *
     */
    @Parameter(names = Arguments.LOG_FILE, description = "Zookeeper transaction log file path")
    public String logFile;

    /**
     * Zookeeper configuration file. Zookeeper configuration file tells
     * zookeeper where to store transaction log files and snapshot files.
     * zklogtool can read locations from configuration file.
     *
     */
    @Parameter(names = Arguments.PROPERTIES_FILE, description = "Zookeeper configuration file path")
    public String propertiesFile;

    /**
     * Decoder that converts znodes byte array to <code>String</code> that can
     * be printed to output. Znode holds data in form of a byte array. That byte
     * array must be decoded in something that can be printed to output in order
     * to display it.
     *
     */
    @Parameter(names = Arguments.DATA_DECODER, description = "Decoder used to display znode's data byte array")
    public String dataDecoder = "UnicodeDecoder";

    /**
     * Holds logic for <b>log</b> command execution.
     *
     * @throws InterruptedException Thrown if interrupted during execution.
     */
    public void execute() throws InterruptedException {

        TransactionLog transactionLog = null;
        TransactionLogReaderFactory factory = new TransactionLogReaderFactory();
        final StringBuilder print = new StringBuilder();
        DataDecoder decoder = null;

        if (dataDecoder.contentEquals("UnicodeDecoder")) {
            decoder = new UnicodeDecoder();
        } else {

            System.err.println("Decoder not recognized");
            exit(1);
        }

        final TransactionPrinter printer = new TransactionPrinter(print, decoder);

        if (dataLogDir != null) {

            File transactionLogDir = new File(dataLogDir);

            if (!checkDirectoryValid(transactionLogDir)) {
                exit(1);
            }

            TransactionLogFileList l = new DataDirTransactionLogFileList(transactionLogDir);

            transactionLog = new TransactionLog(l, factory);

        } else if (logFile != null) {

            File transactionLogFile = new File(logFile);

            //check file
            if (!checkFileValid(transactionLogFile)) {
                exit(1);
            }

            transactionLog = new TransactionLog(transactionLogFile, factory);

        } else if (propertiesFile != null) {

            File properties = new File(propertiesFile);

            if (!checkFileValid(properties)) {
                exit(1);
            }

            PropertiesReader propertiesReader = null;

            try {
                propertiesReader = new PropertiesReader(properties);
            } catch (IOException e) {
                System.err.println("Problem with reading properties file: " + properties.getAbsolutePath());
                exit(1);
            }

            String transactionLogDirPath = propertiesReader.getTransactionLogDir();

            if (transactionLogDirPath == null) {

                System.err.println("Problem in properties file: " + properties.getAbsolutePath());
                exit(1);

            }

            File transactionLogDir = new File(transactionLogDirPath);

            if (!checkDirectoryValid(transactionLogDir)) {
                exit(1);
            }

            TransactionLogFileList l = new DataDirTransactionLogFileList(transactionLogDir);

            transactionLog = new TransactionLog(l, factory);

        }

        if (follow) {

            TransactionMonitor ts = new TransactionMonitor(transactionLog);

            ts.addListener(new TransactionListener() {

                @Override
                public void onTransaction(Transaction t) {

                    printer.print(t);
                    System.out.println(print);
                    print.setLength(0);

                }

                @Override
                public void onPartialTransaction() {

                    //do nothing
                }

                @Override
                public void onCorruption() {

                    //print error and exit
                    System.err.println("Data corruption");
                    exit(1);

                }

            });

            if (startWithLastTransaction) {

                ts.startAtLastTransaction();

            } else {

                ts.startAtFirstTransaction();

            }

            //there must be a better way
            synchronized (this) {
                wait();
            }

            //not really needed
            exit(0);

        } else {

            TransactionIterator transactionIterator = transactionLog.iterator();

Answer:             while (transactionIterator.nextTransactionState() == TransactionState.OK) {