jdrum-commons/src/main/java/at/rovo/drum/util/KeyComparator.java
public class KeyComparator<T extends DrumStoreEntry<?, ?>> implements Comparator<T>, Serializable {

    private static final long serialVersionUID = -687991492884005033L;

    @Override
    public int compare(T o1, T o2) {
        if (o1.getKey() < o2.getKey()) {
            return -1;
        } else if (o1.getKey() > o2.getKey()) {
            return 1;
        }
        return 0;
    }
}
jdrum-datastore-simple/src/test/java/at/rovo/drum/datastore/simple/utils/Pair.java
public class Pair<A, B> {
    private A a = null;
    private B b = null;

    /**
     * Default Constructor
     */
    public Pair() {

    }

    /**
     * Creates a new instance of this class and sets the first element as a, and the second element as b
     *
     * @param a The first element of the pair
     * @param b The second element of the pair
     */
    public Pair(A a, B b) {
        this.a = a;
        this.b = b;
    }

    /**
     * Returns the first element of this pair
     *
     * @return The first element of this pair
     */
    public A getFirst() {
        return a;
    }

    /**
     * Returns the last element of this pair
     *
     * @return The last element of this pair
     */
    public B getLast() {
        return b;
    }

    /**
     * Sets the first element of this pair
     *
     * @param a The first element of this pair
     */
    public void setFirst(A a) {
        this.a = a;
    }

    /**
     * Sets the last element of this pair
     *
     * @param b The last element of this pair
     */
    public void setLast(B b) {
        this.b = b;
    }
}
jdrum-datastore-simple/src/test/java/at/rovo/drum/datastore/simple/utils/DataStoreUtils.java
public final class DataStoreUtils {

    private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());

    /**
     * Iterates through the content of the data store and returns a list of contained hash keys for the respective
     * entries.
     *
     * @param name       The name of the backing data store
     * @param valueClass The data type of the value object associated to the key
     * @param <V>        The type of the contained value object
     * @return Returns the keys of the contained objects in the data store
     * @throws IOException   If any error during reading the data store occurs
     * @throws DrumException Thrown if the next entry from the data store could not be extracted
     */
    public static <V> List<Long> getStoredKeys(String name, Class<? extends V> valueClass)
            throws IOException, DrumException {
        Map<Long, V> content = getContentAsMap(name, valueClass);
        List<Long> keys = new ArrayList<>(content.size());
        keys.addAll(content.keySet());
        return keys;
    }

    public static <V> void printCacheContent(Map<Long, V> content) {
        LOG.debug("Data contained in cache file: ");
        for (Long key : content.keySet()) {
            LOG.debug("Key: {}, Value: {}", key, content.get(key));
        }
    }

    public static <V> void printCacheContent(String name, Class<? extends V> valueClass)
            throws IOException, DrumException {
        Map<Long, V> content = getContentAsMap(name, valueClass);
        printCacheContent(content);
    }

    /**
     * Iterates through the content of the data store and adds each entry as new entry to a {@link Map} with the entries
     * hash key as key and the corresponding data entry as value.
     *
     * @param name       The name of the backing data store
     * @param valueClass The data type of the value object associated to the key
     * @param <V>        The type of the contained value object
     * @return Returns the content of the data store as a map object
     * @throws IOException   If any error during reading the data store occurs
     * @throws DrumException Thrown if the next entry from the data store could not be extracted
     */
    public static <V> Map<Long, V> getContentAsMap(String name, Class<? extends V> valueClass)
            throws IOException, DrumException {
        LOG.info("Data contained in {}/cache.db:", name);

        Map<Long, V> content = new LinkedHashMap<>();
        try (RandomAccessFile dataStore = DataStoreUtils.openDataStore(name)) {
            dataStore.seek(0);

            Pair<Long, V> data = DataStoreUtils.getNextEntry(dataStore, valueClass);
            for (; data != null; data = DataStoreUtils.getNextEntry(dataStore, valueClass)) {
                V value = data.getLast();
                content.put(data.getFirst(), value);
                if (value != null) {
                    LOG.info("Key: {}, Value: {}", data.getFirst(), value);
                } else {
                    LOG.info("Key: {}, Value: {}", data.getFirst(), null);
                }
            }
        }
        return content;
    }

    /**
     * Opens the backing data store and returns a {@link RandomAccessFile} reference to the opened file.
     *
     * @param name The name of the DRUM instance the data store should be opened for
     * @return A reference to the random access file
     * @throws IOException If the file could not be accessed
     */
    public static RandomAccessFile openDataStore(String name) throws IOException {
        String userDir = System.getProperty("user.dir");
        String cacheName = userDir + "/cache/" + name + "/cache.db";
        return new RandomAccessFile(cacheName, "r");
    }

    /**
     * Returns the next key/value pair from the backing data store.
     *
     * @param cacheFile  The name of the backing data store
     * @param valueClass The data type of the value object associated to the key
     * @param <V>        The type of the contained value object
     * @return A key/value tuple
     * @throws DrumException Thrown if either an IOException occurs during fetching the next Entry or the data can't be deserialized
     *                       from bytes to an actual object
     */
    public static <V> Pair<Long, V> getNextEntry(RandomAccessFile cacheFile, Class<? extends V> valueClass)
            throws DrumException {
        // Retrieve the key from the file
        try {
            if (cacheFile.getFilePointer() >= cacheFile.length()) {
                return null;
            }

            Long key = cacheFile.readLong();

            // Retrieve the value from the file
            int valueSize = cacheFile.readInt();
            if (valueSize > 0) {
                byte[] byteValue = new byte[valueSize];
                cacheFile.read(byteValue);
                V value;
                // as we have our own serialization mechanism, we have to ensure that these objects are serialized
                // appropriately
                if (Serializable.class.isAssignableFrom(valueClass)) {
                    value = DrumUtils.deserialize(byteValue, valueClass);
                }
                // should not happen - but in case we refactor again leave it in
                else {
                    throw new DrumException("Could not read next entry as value was null before reading its bytes");
                }
                return new Pair<>(key, value);
            }
            return new Pair<>(key, null);
        } catch (IOException | ClassNotFoundException e) {
            throw new DrumException("Error fetching next entry from cache", e);
        }
    }
}
jdrum-commons/src/main/java/at/rovo/drum/DrumStoreEntry.java
public interface DrumStoreEntry<V extends Serializable, A extends Serializable> extends Serializable {

    /**
     * Returns the key of the current data object.
     *
     * @return The key of the data object
     */
    Long getKey();

    /**
     * Sets the key of the current data object
     *
     * @param key The new key of the data object
     */
    void setKey(Long key);

    /**
     * Returns the value of the current data object.
     *
     * @return The value object related to the key of this data object
     */
    V getValue();

    /**
     * Sets the value of the current data object.
     *
     * @param value The new value of the data object
     */
    void setValue(V value);

    /**
     * Returns the additional information attached to a key for the current data object.
     *
     * @return The additional information of the data object
     */
    A getAuxiliary();

    /**
     * Sets the additional information attached to a key for the current data object.
     *
     * @param aux The additional information of the data object
     */
    void setAuxiliary(A aux);

    /**
     * Returns the DRUM operation to be used on this data object.
     *
     * @return The DRUM operation to be used
     */
    DrumOperation getOperation();

    /**
     * Appends the value of the provided data object to the value of this data object.
     *
     * @param data The data object containing the value to append to the value of this data object
     * @throws IllegalArgumentException thrown if the provided data to append is null
     * @throws NotAppendableException   Thrown if the data object to append is not an instance of {@link AppendableData}
     */
    void appendValue(V data) throws NotAppendableException;

    /**
     * Returns the key of this data object as an 8 byte long array in big-endian order.
     *
     * @return The key of this data object as byte-array
     */
    byte[] getKeyAsBytes();

    /**
     * Returns the value of this data object as a byte-array in big endian order.
     *
     * @return The value of this data object as byte-array
     */
    byte[] getValueAsBytes();

    /**
     * Returns the attached data to a key for this data object as a byte-array in big endian order.
     *
     * @return The attached data to a key as byte-array
     */
    byte[] getAuxiliaryAsBytes();

    /**
     * Sets the result for the defined DRUM operation based on the data of this object.
     *
     * @param result The result for this data object
     */
    void setResult(DrumResult result);

    /**
     * Returns the result gathered by DRUM based on the provided data object.
     *
     * @return The result for this data object
     */
    DrumResult getResult();

    /**
     * Returns the length of the key and the value in the byte-array.
     *
     * @return The length of the key and value in the byte-array
     */
    long getByteLengthKV();
}
jdrum-datastore-base/src/main/java/at/rovo/drum/base/InMemoryEntry.java
public class InMemoryEntry<V extends Serializable, A extends Serializable>
        extends PositionAware
        implements DrumStoreEntry<V, A> {

    /**
     * The logger of this class
     */
    private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());

    /**
     * The key of the statement
     */
    private Long key;
    /**
     * The value belonging to the key
     */
    private V value;
    /**
     * Additional information related to the key
     */
    private A aux;
    /**
     * The DRUM operation to execute on the data
     */
    private DrumOperation operation;
    /**
     * The result extracted based on the data and the executed DRUM operation
     */
    private DrumResult result = null;

    /**
     * Creates a new instance of this class without instantiating any field.
     */
    public InMemoryEntry() {

    }

    /**
     * Creates a new instance of this class and initializes required fields.
     *
     * @param key       The key of this data object
     * @param value     The value of this data object
     * @param aux       The auxiliary data attached to the key
     * @param operation The DRUM operation to execute for this data object
     */
    public InMemoryEntry(Long key, V value, A aux, DrumOperation operation) {
        this.key = key;
        this.value = value;
        this.aux = aux;
        this.operation = operation;
    }

    @Override
    public Long getKey() {
        return this.key;
    }

    @Override
    public void setKey(Long key) {
        this.key = key;
    }

    @Override
    public V getValue() {
        return this.value;
    }

    @Override
    public void setValue(V value) {
        this.value = value;
    }

    @Override
    public A getAuxiliary() {
        return this.aux;
    }

    @Override
    public void setAuxiliary(A aux) {
        this.aux = aux;
    }

    @Override
    public DrumOperation getOperation() {
        return this.operation;
    }

    /**
     * Sets the DRUM operation to be used on this data object.
     *
     * @param operation The DRUM operation to be used
     */
    public void setOperation(DrumOperation operation) {
        this.operation = operation;
    }

    @SuppressWarnings("unchecked")
    @Override
    public void appendValue(V data) throws NotAppendableException {
        if (data == null) {
            throw new IllegalArgumentException("Cannot append null value");
        }

        if (null != this.value) {
            if (this.value instanceof AppendableData) {
                ((AppendableData<V>) this.value).append(data);
            } else {
                throw new NotAppendableException(
                        "Value data does not implement at.rovo.caching.drum.AppendableData interface!");
            }
        } else {
            // in case the value was null before and data should be appended, the current value of this instance should
            // contain the appended value
            this.value = data;
        }
    }

    @Override
    public byte[] getKeyAsBytes() {
        return DrumUtils.long2bytes(key);
    }

    @Override
    public byte[] getValueAsBytes() {
        if (this.value == null) {
            return null;
        }

        byte[] bytes;
        try {
            bytes = DrumUtils.serialize(this.value);
        } catch (IOException ioEx) {
            LOG.warn("Could not turn value {} into a byte array for entry with key {}", this.value, this.key);
            bytes = new byte[0];
        }
        return bytes;
    }

    /**
     * Returns the attached data to a key for this data object as a byte-array in big endian order.
     *
     * @return The attached data to a key as byte-array
     */
    @Override
    public byte[] getAuxiliaryAsBytes() {
        if (this.aux == null) {
            return null;
        }

        byte[] bytes;
        try {
            bytes = DrumUtils.serialize(this.aux);
        } catch (IOException ioEx) {
            LOG.warn("Could not turn auxiliary data entry {} into a byte array for entry with key {}",
                    this.aux, this.key);
            bytes = new byte[0];
        }
        return bytes;
    }

    @Override
    public void setResult(DrumResult result) {
        this.result = result;
    }

    @Override
    public DrumResult getResult() {
        return this.result;
    }

    @Override
    public long getByteLengthKV() {
        // the 8 byte long key and the 4 byte long integer value representing the length of bytes the value field does
        // take
        long bytes = 12;
        if (this.value != null) {
            bytes += this.getValueAsBytes().length;
        }

        return bytes;
    }

    @Override
    public String toString() {
        return "op: " + this.operation + " key: " + this.key + " value: " + this.value + " aux: " + this.aux +
                " result: " + this.result;
    }
}
jdrum-datastore-simple/src/test/java/at/rovo/drum/datastore/simple/utils/CacheFileDeleter.java
public class CacheFileDeleter extends SimpleFileVisitor<Path> {

    @Override
    public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
        Files.delete(file);
        return FileVisitResult.CONTINUE;
    }

    @Override
    public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
        // try to delete the file anyway, even if its attributes could not be read, since delete-only access is
        // theoretically possible
        Files.delete(file);
        return FileVisitResult.CONTINUE;
    }

    @Override
    public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
        if (exc == null) {
            Files.delete(dir);
            return FileVisitResult.CONTINUE;
        } else {
            // directory iteration failed; propagate exception
            throw exc;
        }
    }
}
jdrum-datastore-simple/src/test/java/at/rovo/drum/datastore/simple/utils/PLDTestData.java
@SuppressWarnings("Duplicates")
public class PLDTestData implements AppendableData<PLDTestData>, Comparable<PLDTestData> {

    private long hash = 0;
    private int budget = 0;
    private Set<Long> indegreeNeighbors;
    private transient String pld = null;

    public PLDTestData() {
        this.indegreeNeighbors = new LinkedHashSet<>();
    }

    public PLDTestData(long hash, int budget, Set<Long> indegreeNeighbors) {
        this.hash = hash;
        this.budget = budget;
        this.indegreeNeighbors = indegreeNeighbors;
    }

    public long getHash() {
        return this.hash;
    }

    public int getIndegree() {
        return this.indegreeNeighbors.size();
    }

    public int getBudget() {
        return this.budget;
    }

    public Set<Long> getIndegreeNeighbors() {
        return this.indegreeNeighbors;
    }

    public String getPLD() {
        return this.pld;
    }

    public void setHash(long hash) {
        this.hash = hash;
    }

    public void setBudget(int budget) {
        this.budget = budget;
    }

    public void setIndegreeNeighbors(Set<Long> indegreeNeighbors) {
        this.indegreeNeighbors = indegreeNeighbors;
    }

    public void setPLD(String pld) {
        this.pld = pld;
    }

    public void addIndegreeNeighbors(Set<Long> indegreeNeighbors) {
        this.indegreeNeighbors.addAll(indegreeNeighbors);
    }

    @Override
    public boolean equals(Object obj) {
        if (obj instanceof PLDTestData) {
            PLDTestData data = (PLDTestData) obj;
            return data.getHash() == this.hash;
        }
        return false;
    }

    @Override
    public int hashCode() {
        int result = 17;
        result = 31 * result + Long.valueOf(this.hash).hashCode();
        return result;
    }

    @Override
    public String toString() {
        StringBuilder buffer = new StringBuilder();
        buffer.append("Hash: ");
        buffer.append(this.hash);
        buffer.append("; neighbors: {");
        int size = this.indegreeNeighbors.size();
        int i = 0;
        for (Long neighbor : this.indegreeNeighbors) {
            i++;
            buffer.append(neighbor);
            if (i != size) {
                buffer.append(", ");
            }
        }
        buffer.append("}");

        return buffer.toString();
    }

    @Override
    public void append(PLDTestData data) {
        if (this.indegreeNeighbors == null) {
            this.indegreeNeighbors = new TreeSet<>();
        }
        if (data != null) {
            this.indegreeNeighbors.addAll(data.getIndegreeNeighbors());
        }
    }

    private void writeObject(java.io.ObjectOutputStream stream) throws java.io.IOException {
        // 8 bytes long - hash
        stream.writeLong(this.hash);
        // 4 bytes int - length of indegreeNeighobrs
        stream.writeInt(this.indegreeNeighbors.size());
        for (Long neighbor : this.indegreeNeighbors) {
            // 8 bytes long - neighbor hash
            stream.writeLong(neighbor);
        }
        // 4 bytes int - budget value
        stream.writeInt(this.budget);
    }

    private void readObject(java.io.ObjectInputStream stream) throws java.io.IOException, ClassNotFoundException {
        this.indegreeNeighbors = new LinkedHashSet<>();

        // read hash
        this.hash = stream.readLong();
        // read neighbors length
        int neighborsLength = stream.readInt();
        for (int i = 0; i < neighborsLength; i++) {
            long neighborHash = stream.readLong();
            this.indegreeNeighbors.add(neighborHash);
        }
        this.budget = stream.readInt();
    }

    @Override
    public byte[] toBytes() {
        int size = 12 + 8 * this.indegreeNeighbors.size() + 4;
        byte[] totalBytes = new byte[size];
        byte[] keyBytes = DrumUtils.long2bytes(this.hash); // 8 bytes
        System.arraycopy(keyBytes, 0, totalBytes, 0, 8);
        byte[] neighborSize = DrumUtils.int2bytes(this.indegreeNeighbors.size());
        System.arraycopy(neighborSize, 0, totalBytes, 8, 4); // 4 bytes
        int pos = 12;
        for (Long neighbor : this.indegreeNeighbors) {
            byte[] neighborBytes = DrumUtils.long2bytes(neighbor);
            System.arraycopy(neighborBytes, 0, totalBytes, pos, 8);
            pos += 8;
        }
        byte[] budget = DrumUtils.int2bytes(this.budget);
        System.arraycopy(budget, 0, totalBytes, pos, 4); // 4 bytes

        return totalBytes;
    }

    @Override
    public PLDTestData readBytes(byte[] bytes) {
        byte[] keyBytes = new byte[8];
        System.arraycopy(bytes, 0, keyBytes, 0, 8);
        long hash = DrumUtils.byte2long(keyBytes);

        byte[] valueSizeBytes = new byte[4];
        System.arraycopy(bytes, 8, valueSizeBytes, 0, 4);
        int valueSize = DrumUtils.bytes2int(valueSizeBytes);

        TreeSet<Long> indegreeNeighbors = new TreeSet<>();

        int pos = 12;
        for (int i = 0; i < valueSize; i++) {
            byte[] valueBytes = new byte[8];
            System.arraycopy(bytes, pos, valueBytes, 0, 8);
            indegreeNeighbors.add(DrumUtils.byte2long(valueBytes));
            pos += 8;
        }

        byte[] budgetBytes = new byte[4];
        System.arraycopy(bytes, pos, budgetBytes, 0, 4);
        int budget = DrumUtils.bytes2int(budgetBytes);

        PLDTestData data = new PLDTestData();
        data.setHash(hash);
        data.setIndegreeNeighbors(indegreeNeighbors);
        data.setBudget(budget);

        return data;
    }

    @Override
    public int compareTo(PLDTestData o) {
        if (this.getHash() < o.getHash()) {
            return -1;
        } else if (this.getHash() > o.getHash()) {
            return 1;
        }

        return 0;
    }
}
jdrum-commons/src/main/java/at/rovo/drum/DrumOperation.java
public enum DrumOperation {

    /**
     * Defines that the cache should be checked against a key for its availability.
     * <p>
     * If one is already available {@link DrumResult#DUPLICATE_KEY} should be triggered, else {@link
     * DrumResult#UNIQUE_KEY} has to be returned
     */
    CHECK('c'),
    /**
     * Defines that a value for a cached key needs to be updated. If the key is not yet stored in the cache it should be
     * created instead with the given value. If the key is present it will be replaced by the new entry.
     */
    UPDATE('u'),
    /**
     * Marks a certain element to be {@link #CHECK}ed first and then {@link #UPDATE}d afterwards.
     */
    CHECK_UPDATE('b'),
    /**
     * Defines that a value for a cached key needs to be updated. If the key is not yet stored in the cache it should be
     * created instead with the given value. If the key is already present it will append the data of the new data
     * element to the content of the already stored data entry instead of replacing the entry.
     */
    APPEND_UPDATE('a');

    private final char c;

    DrumOperation(char c) {
        this.c = c;
    }

    /**
     * Returns the token for the respective DRUM operation.
     *
     * @return A character identifying the current DRUM operation
     */
    public final char getTokenForOperation() {
        return c;
    }

    /**
     * Returns a DRUM operation for the given token. If the token is unknown a {@link DrumException} will be thrown.
     *
     * @param c The character representing the DRUM operation
     * @return The matching DRUM operation
     * @throws DrumException If the provided token does not match a valid DRUM operation
     */
    public static DrumOperation fromToken(char c) throws DrumException {
        DrumOperation op;
        switch (c) {
            case 'c':
                op = DrumOperation.CHECK;
                break;
            case 'u':
                op = DrumOperation.UPDATE;
                break;
            case 'b':
                op = DrumOperation.CHECK_UPDATE;
                break;
            case 'a':
                op = DrumOperation.APPEND_UPDATE;
                break;
            default:
                throw new DrumException("Invalid DRUM operation token received: " + c);
        }
        return op;
    }
}
jdrum-datastore-simple/src/test/java/at/rovo/drum/datastore/simple/utils/DataStoreMatcher.java
public class DataStoreMatcher {

    /**
     * The logger of this class
     */
    private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());

    /**
     * Helper class to encapsulate the comparison state during execution of the <code>matchesSafely</code> method.
     *
     * @param <T> The type of the value object
     */
    private static class Status<T> {

        private enum MismatchReason {
            LENGTH_VIOLATION,
            KEY_MISMATCH,
            VALUE_MISMATCH
        }

        MismatchReason mismatchReason;
        int actualLength;
        Long actualKey;
        T actualValue;

        int expectedLength;
        Long expectedKey;
        T expectedValue;

        int entryNum;
    }

    /**
     * Compares a map representation of a {@link Long} key and {@link T} value with an expected state provided as
     * a list of {@link Pair} elements which contain the {@link Long} key as first argument and the actual {@link T}
     * value as second argument.
     *
     * @param expected A list of contained {@link Pair} elements which encapsulate a {@link Long} key and a {@link T} value
     * @param <T>      The type of the value object
     * @return Return a Hamcrest {@link Matcher} object which will compare the provided elements with an actual one.
     */
    public static <T> Matcher<Map<Long, T>> containsEntries(List<Pair<Long, T>> expected) {
        Status<T> status = new Status<>();

        return new TypeSafeMatcher<Map<Long, T>>() {
            @Override
            protected boolean matchesSafely(Map<Long, T> actual) {
                try {
                    if (expected.size() != actual.size()) {
                        status.mismatchReason = Status.MismatchReason.LENGTH_VIOLATION;
                        status.actualLength = actual.size();
                        status.expectedLength = expected.size();

                        // the actual length of the expected and actual entries differs
                        return false;
                    }

                    int expectedPos = 0;
                    for (Long key : actual.keySet()) {
                        // check that the n'th expected key matches the m'th actual key
                        if (!expected.get(expectedPos).getFirst().equals(key)) {
                            status.mismatchReason = Status.MismatchReason.KEY_MISMATCH;
                            status.actualKey = key;
                            status.expectedKey = expected.get(expectedPos).getFirst();
                            status.entryNum = expectedPos;

                            return false;
                        }
                        // check that the n'th expected value matches the m'th actual value
                        if (expected.get(expectedPos).getLast() == null && actual.get(key) != null
                                || expected.get(expectedPos).getLast() != null && actual.get(key) != null
                                && !expected.get(expectedPos).getLast().equals(actual.get(key))) {
                            status.mismatchReason = Status.MismatchReason.VALUE_MISMATCH;
                            status.actualValue = actual.get(key);
                            status.expectedValue = expected.get(expectedPos).getLast();
                            status.entryNum = expectedPos;

                            return false;
                        }
                        ++expectedPos;
                    }
                } catch (Exception ex) {
                    LOG.error("Caught exception while comparing the content of the data store", ex);
                    ex.printStackTrace();
                    return false;
                }
                return true;
            }

            @Override
            public void describeTo(Description description) {
                if (Status.MismatchReason.LENGTH_VIOLATION == status.mismatchReason) {
                    description.appendText("data store to contain ").appendValue(status.expectedLength).appendText(" entries ");
                } else if (Status.MismatchReason.KEY_MISMATCH == status.mismatchReason) {
                    description.appendText("data store to contain element with key " + status.expectedKey + " at position " + status.entryNum);
                } else {
                    description.appendText("data store to contain element with value " + status.expectedValue + " at position " + status.entryNum);
                }
            }

            @Override
            protected void describeMismatchSafely(final Map<Long, T> actual, final Description mismatchDescription) {
                if (Status.MismatchReason.LENGTH_VIOLATION == status.mismatchReason) {
                    mismatchDescription.appendText("found ").appendValue(status.actualLength);
                    if (status.expectedLength > status.actualLength) {
                        mismatchDescription.appendText(" only");
                    }
                } else if (Status.MismatchReason.KEY_MISMATCH == status.mismatchReason) {
                    mismatchDescription.appendText("found key " + status.actualKey + " at this position");
                } else {
                    mismatchDescription.appendText("found value " + status.actualValue + " at this position");
                }
            }
        };
    }
}
jdrum-commons/src/main/java/at/rovo/drum/util/DrumUtils.java
public class DrumUtils {

    /**
     * Calculates a 8-byte (64bit) hash from a provided input {@link String}.
     *
     * @param string The string object to generate the 64bit hash value for
     * @return The 64bit long hash value for the provided string
     */
    public static long hash(final String string) {
        long h = 1125899906842597L; // prime
        int len = string.length();

        for (int i = 0; i < len; i++) {
            h = 31 * h + string.charAt(i);
        }
        return h;
    }

    /**
     * Calculates a 8-byte (64bit) hash from an object by invoking the objects {@link Object#toString()} first and then
     * generating the hash value for the generated string value.
     *
     * @param object The object whose key shall be calculated
     * @return The 64bit long hash value for the provided object
     */
    public static long hash(final Object object) {
        return hash(object.toString());
    }

    /**
     * Calculates the bucket id the key should be placed in according to the first n bits of the key where n is the
     * power of 2 numBuckets is defined as. F.e if 1024 buckets are defined the first 10 bits of the key will be taken
     * as the index for the actual bucket (2^10 = 1024, 2^9 = 512, ...)
     *
     * @param key        The 64bit key whose bucket index should be calculated
     * @param numBuckets The total number of available buckets. This should be a power of two (e.g. 2, 4, 8, 16, 32, ...)
     * @return The bucket index the key should be in
     * @throws IllegalArgumentException If the provided input parameter is not a power of 2
     */
    public static int getBucketForKey(long key, int numBuckets) {
        // test if numBuckets is a power of 2
        int exponent = Math.getExponent(numBuckets);
        if (numBuckets != Math.pow(2, exponent)) {
            throw new IllegalArgumentException("Number of buckets does not correspond to a power of 2!");
        }
        return (int) (key >> (64 - exponent)) + numBuckets / 2;
    }

    /**
     * Calculates the bucket based on a 64bit key.
     * <p>
     * The calculation is based on an example presented by Leandro T C Melo
     *
     * @param key        The key whose bucket should be calculated
     * @param numBuckets The total number of available buckets. This should be a power of two (e.g. 2, 4, 8, 16, 32, ...)
     * @return The bucket the key should be in
     * @throws IllegalArgumentException If the provided input parameter is not a power of 2
     * @see <a href="http://www.codeproject.com/Articles/36221/DRUM-A-C-Implementation-for-the-URL-seen-Test-of-a">
     *     DRUM - A C++ Implementation for the URL-seen Test of a Web Crawler</a>
     */
    public static long getBucketOfKey(long key, int numBuckets) {
        int exponent = Math.getExponent(numBuckets);
        // Build mask-string
        StringBuilder builder = checkExponentAndPrepareMaskString(numBuckets, exponent);
        for (int i = 0; i < 64; i++) {
            builder.append(i < exponent ? "1" : "0");
        }
        long mask = new BigInteger(builder.toString(), 2).longValue();
        long bucket = mask & key;
        return (bucket >>> (64 - exponent));
    }

    /**
     * Calculates the bucket based on a 32bit key.
     * <p>
     * The calculation is based on an example presented by Leandro T C Melo
     *
     * @param key        The key whose bucket should be calculated
     * @param numBuckets The total number of available buckets. This should be a power of two (e.g. 2, 4, 8, 16, 32, ...)
     * @return The bucket the key should be in
     * @throws IllegalArgumentException If the provided input parameter is not a power of 2
     * @see <a href="http://www.codeproject.com/Articles/36221/DRUM-A-C-Implementation-for-the-URL-seen-Test-of-a">
     *     DRUM - A C++ Implementation for the URL-seen Test of a Web Crawler</a>
     */
    public static int getBucketOfKey(int key, int numBuckets) {
        int exponent = Math.getExponent(numBuckets);
        // Build mask-string
        StringBuilder builder = checkExponentAndPrepareMaskString(numBuckets, exponent);
        for (int i = 0; i < 32; i++) {
            builder.append(i < exponent ? "1" : "0");
        }
        int mask = new BigInteger(builder.toString(), 2).intValue();
        int bucket = mask & key;
        return (bucket >>> (32 - exponent));
    }

    private static StringBuilder checkExponentAndPrepareMaskString(int numBuckets, int exponent) {
        if (numBuckets != Math.pow(2, exponent)) {
            throw new IllegalArgumentException("Number of buckets does not correspond to a power of 2!");
        }
        // Build mask-string
        return new StringBuilder();
    }

    /**
     * Serializes an object into a byte-array.
     *
     * @param obj The object to convert into a binary array
     * @return The bytes of the object or an empty byte array if the source object was null
     * @throws IOException If any error during the serialization occurs
     */
    public static byte[] serialize(Object obj) throws IOException {
        if (obj instanceof String) {
            return obj.toString().getBytes(StandardCharsets.UTF_8);
        }
        // objects implementing the ByteSerializable interface do their own homework on serializing/deserializing their
        // state to/from bytes. This avoids returning unnecessary bytes
        if (obj instanceof ByteSerializable) {
            return ((ByteSerializable) obj).toBytes();
        }
        // no custom serialization available, fall back to the default one although it is wasteful in relation to the
        // number of actual bytes produced! Simple Integer objects create up to 81 bytes instead of only 4!
        try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
             ObjectOutputStream oos = new ObjectOutputStream(baos)) {
            if (obj == null) {
                return new byte[0];
            }
            oos.writeObject(obj);
            oos.flush();

            return baos.toByteArray();
        }
    }

    /**
     * Turns a byte-array into an object
     *
     * @param bytes The bytes of the object
     * @param type The class for which the provided <em>bytes</em> should be transformed into a new instance for
     * @param <V> The type of the value to deserialize into
     * @return The object deserialized from the array of bytes provided
     * @throws IOException If the cast of the bytes to a {@link ByteSerializable} object failed
     * @throws ClassNotFoundException If the class requested via the <em>type</em> parameter could not be found
     */
    public static <V> V deserialize(byte[] bytes, Class<? extends V> type)
            throws IOException, ClassNotFoundException {
        V ret;
        // check if the byte array is a String (character array)
        if (String.class.isAssignableFrom(type)) {
            ret = type.cast(new String(bytes, StandardCharsets.UTF_8));
        } else if (ByteSerializable.class.isAssignableFrom(type)) {
            try {
                ret = type.cast(((ByteSerializable) type.getConstructor().newInstance()).readBytes(bytes));
            } catch (Exception ex) {
                throw new IOException(ex);
            }
        } else {
            try (ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
                 ObjectInputStream ois = new ObjectInputStream(bais)) {
                Object obj = ois.readObject();
                ret = type.cast(obj);
            }
        }
        return ret;
    }

    /**
     * Converts a long value into a 8 byte (64bit) long byte array.
     *
     * @param l The long value to be converted into a byte array
     * @return the 8-byte array representing the long value
     */
    public static byte[] long2bytes(long l) {
        byte[] result = new byte[8];

        for (int i = 0; i < result.length; i++) {
            result[i] = (byte) (l >> ((8 * result.length) - (8 * (i + 1))));
        }
        return result;
    }

    /**
     * Converts an integer value into a 4 byte (32bit) long byte array.
     *
     * @param i The integer value to be converted to a byte array
     * @return The 4-byte array representing the integer value
     */
    public static byte[] int2bytes(int i) {
        byte[] result = new byte[4];

        result[0] = (byte) (i >> 24);
        result[1] = (byte) (i >> 16);
        result[2] = (byte) (i >> 8);
        result[3] = (byte) (i /* >> 0 */);
        return result;
    }

    /**
     * Converts a 4 byte long byte array into an integer value
     *
     * @param b The 4 byte array containing the bits of the integer
     * @return The converted integer object
     */
    public static int bytes2int(byte[] b) {
        return new BigInteger(b).intValue();
    }

    /**
     * Converts a 8 byte long byte array into a long value
     *
     * @param b the 8 byte array containing the bits of the long value
     * @return the converted long object
     */
    public static long byte2long(byte[] b) {
        return new BigInteger(b).longValue();
    }
}


Question: import at.rovo.drum.DrumOperation;
import at.rovo.drum.DrumStoreEntry;
import at.rovo.drum.base.InMemoryEntry;
import at.rovo.drum.datastore.simple.utils.CacheFileDeleter;
import at.rovo.drum.datastore.simple.utils.DataStoreMatcher;
import at.rovo.drum.datastore.simple.utils.DataStoreUtils;
import at.rovo.drum.datastore.simple.utils.PLDTestData;
import at.rovo.drum.datastore.simple.utils.Pair;
import at.rovo.drum.util.DrumUtils;
import at.rovo.drum.util.KeyComparator;
import java.io.File;
import java.io.IOException;
import java.lang.invoke.MethodHandles;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import org.hamcrest.MatcherAssert;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
            expected.add(new Pair<>(-408508820557862601L, "Noch ein Test"));  // ... 8+4+13 bytes
            expected.add(new Pair<>(356380646382129811L, "test"));           // ... 8+4+4 bytes

            assertThat(dataStore.length(), is(equalTo(105L)));
            MatcherAssert.assertThat(dataStoreContent, DataStoreMatcher.containsEntries(expected));

            dataStore.reset();

            LOG.debug("Changing data item with key: {} from '{}' to 'null'",
                    dataList.get(4).getKey(), dataList.get(4).getKey());
            dataList.get(4).setValue(null);
            dataStore.writeEntry(dataList.get(4));

            // print the current content of the data store
            dataStoreContent = (LinkedHashMap<Long, String>) DataStoreUtils.getContentAsMap("test", String.class);
            DataStoreUtils.printCacheContent(dataStoreContent);

            expected = new ArrayList<>();
            expected.add(new Pair<>(-8389167973104044848L, null));             // ... 8+4 bytes
            expected.add(new Pair<>(-8388954286180259435L, null));             // ... 8+4 bytes
            expected.add(new Pair<>(-7747270999347618272L, null));             // ... 8+4 bytes
            expected.add(new Pair<>(-7398944357989757338L, null));             // ... 8+4 bytes
            expected.add(new Pair<>(-427820934381562479L, "Test"));           // ... 8+4+4 bytes
            expected.add(new Pair<>(-408508820557862601L, "Noch ein Test"));  // ... 8+4+13 bytes
            expected.add(new Pair<>(356380646382129811L, null));             // ... 8+4 bytes

            assertThat(dataStore.length(), is(equalTo(101L)));
            MatcherAssert.assertThat(dataStoreContent, DataStoreMatcher.containsEntries(expected));

            dataStore.reset();

            LOG.debug("Changing data item with key: {} from '{}' to 'test'",
                    dataList.get(4).getKey(), dataList.get(4).getValue());
            dataList.get(4).setValue("test");
            dataStore.writeEntry(dataList.get(4));

            // print the current content of the data store
            dataStoreContent = (LinkedHashMap<Long, String>) DataStoreUtils.getContentAsMap("test", String.class);
            DataStoreUtils.printCacheContent(dataStoreContent);

            expected = new ArrayList<>();
            expected.add(new Pair<>(-8389167973104044848L, null));             // ... 8+4 bytes
            expected.add(new Pair<>(-8388954286180259435L, null));             // ... 8+4 bytes
            expected.add(new Pair<>(-7747270999347618272L, null));             // ... 8+4 bytes
            expected.add(new Pair<>(-7398944357989757338L, null));             // ... 8+4 bytes
            expected.add(new Pair<>(-427820934381562479L, "Test"));           // ... 8+4+4 bytes
            expected.add(new Pair<>(-408508820557862601L, "Noch ein Test"));  // ... 8+4+13 bytes
            expected.add(new Pair<>(356380646382129811L, "test"));           // ... 8+4+4 bytes

            assertThat(dataStore.length(), is(equalTo(105L)));
            MatcherAssert.assertThat(dataStoreContent, DataStoreMatcher.containsEntries(expected));
        }
    }

    @Test
    void testAppendUpdate() throws Exception {
        try (SimpleDataStore<PLDTestData> dataStore = new SimpleDataStoreImpl<>(this.testDir + "/cache.db", "test", PLDTestData.class)) {
            // (1; 2; <3, 7>) has to be read as:
            // key = 1;
            // length of in-degree neighbors = 2;
            // in-degree neighbor set = {3, 7}
            //
            // old file on disk to merge with: (1; 2; <3, 7>), (5; 2; <2, 19>), (76; 4; <5, 13, 22, 88)
            // parameters: (nodeId; number of linked elements; <each linked element>)
            LOG.debug("Creating original data - (1; 2; <3, 7>), (5; 2; <2, 19>), (76; 4; <5, 13, 22, 88)");

            // key: 8 + op: 4 + value: (hash: 8 + neighbor-length: 4 + 2 * neighborHash: 8) + budges: 4) bytes = 44 ... 164 = 120 bytes diff :/
            DrumStoreEntry<PLDTestData, ?> mem1 = createNewData(1L, DrumOperation.UPDATE, 7L, 3L);
            DrumStoreEntry<PLDTestData, ?> mem2 = createNewData(5L, DrumOperation.UPDATE, 2L, 19L);
            DrumStoreEntry<PLDTestData, ?> mem3 = createNewData(76L, DrumOperation.UPDATE, 5L, 13L, 22L, 88L);

            // write the entries
            dataStore.writeEntry(mem1, false);
            dataStore.writeEntry(mem2, false);
            dataStore.writeEntry(mem3, false);

            assertThat(dataStore.length(), is(equalTo(148L)));

            Map<Long, PLDTestData> dataStoreContent = DataStoreUtils.getContentAsMap("test", PLDTestData.class);
            DataStoreUtils.printCacheContent(dataStoreContent);

            List<Pair<Long, PLDTestData>> expected = new ArrayList<>();
            expected.add(new Pair<>(1L, mem1.getValue()));
            expected.add(new Pair<>(5L, mem2.getValue()));
            expected.add(new Pair<>(76L, mem3.getValue()));

            MatcherAssert.assertThat(dataStoreContent, DataStoreMatcher.containsEntries(expected));

            dataStore.reset();

            LOG.debug("Adding new data to integrate into an existing entry - new batch: (5; 4; <2, 3, 7, 88>), (76; 2; <4, 13>)");

            // new batch, sorted as described above: (5; 4; <2, 3, 7, 88>), (76; 2; <4, 13>)
            DrumStoreEntry<PLDTestData, ?> mem2v2 = createNewData(5L, DrumOperation.APPEND_UPDATE, 2L, 3L, 7L, 88L);
            DrumStoreEntry<PLDTestData, ?> mem3v2 = createNewData(76L, DrumOperation.APPEND_UPDATE, 4L, 13L);

            // write the appended entries
            dataStore.writeEntry(mem2v2, true);
            dataStore.writeEntry(mem3v2, true);

            // new file produced in one pass: (1; 2; <3, 7>), (5; 5; <2, 3,  7, 19, 88), (76; 5; <4, 5, 13, 22, 88>)

            dataStoreContent = DataStoreUtils.getContentAsMap("test", PLDTestData.class);
            DataStoreUtils.printCacheContent(dataStoreContent);

            expected = new ArrayList<>();
            expected.add(new Pair<>(1L, new PLDTestData(1L, 0, new TreeSet<>(Arrays.asList(3L, 7L)))));
            expected.add(new Pair<>(5L, new PLDTestData(5L, 0, new TreeSet<>(Arrays.asList(2L, 3L, 7L, 19L, 88L)))));
            expected.add(new Pair<>(76L, new PLDTestData(76L, 0, new TreeSet<>(Arrays.asList(4L, 5L, 13L, 22L, 88L)))));

            MatcherAssert.assertThat(dataStoreContent, DataStoreMatcher.containsEntries(expected));
        }
    }

    private static DrumStoreEntry<String, String> createNewData(String auxiliaryData, String... val) {
        InMemoryEntry<String, String> data = new InMemoryEntry<>();
        if (val != null && val.length > 0) {
            data.setValue(val[0]);
        }
        data.setAuxiliary(auxiliaryData);
Answer:         data.setKey(DrumUtils.hash(data.getAuxiliary()));