All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.jsimpledb.kv.util.KeyListEncoder Maven / Gradle / Ivy


/*
 * Copyright (C) 2015 Archie L. Cobbs. All rights reserved.
 */

package org.jsimpledb.kv.util;

import com.google.common.base.Preconditions;
import com.google.common.collect.AbstractIterator;

import java.io.BufferedInputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Iterator;

import org.jsimpledb.kv.KVPair;
import org.jsimpledb.util.LongEncoder;
import org.jsimpledb.util.UnsignedIntEncoder;

/**
 * Serializes a sequence of {@code byte[]} arrays, compressing consecutive common prefixes.
 *
 * 

* Keys are encoded/decode by {@link #read read()} and {@link #write write()} in one of two forms: *

    *
  • {@code total-length bytes...} *
  • {@code -prefix-length suffix-length suffix-bytes ...} *
* The first length ({@code total-length} or negative {@code prefix-length}) is encoded using {@link LongEncoder}. * The {@code suffix-length}, if present, is encoded using {@link UnsignedIntEncoder}. * *

* Support for encoding and decoding an entire iteration of key/value pairs is supported via * {@link #readPairs readPairs()} and {@link #writePairs writePairs()}. */ public final class KeyListEncoder { private KeyListEncoder() { } /** * Write the next key, compressing its common prefix with the previous key (if any). * * @param out output stream * @param key key to write * @param prev previous key, or null for none * @throws IOException if an I/O error occurs * @throws IllegalArgumentException if {@code out} or {@code key} is null */ public static void write(OutputStream out, byte[] key, byte[] prev) throws IOException { Preconditions.checkArgument(out != null, "null out"); Preconditions.checkArgument(key != null, "null key"); int prefixLength = 0; if (prev != null) { while (prefixLength < key.length && prefixLength < prev.length && key[prefixLength] == prev[prefixLength]) prefixLength++; } if (prefixLength > 1) { final int suffixLength = key.length - prefixLength; LongEncoder.write(out, ~(prefixLength - 2)); UnsignedIntEncoder.write(out, suffixLength); out.write(key, prefixLength, suffixLength); } else { LongEncoder.write(out, key.length); out.write(key); } } /** * Calculate the number of bytes that would be required to write the next key via {@link #write write()}. * * @param key key to write * @param prev previous key, or null for none * @return number of bytes to be written by {@link #write write(out, key, prev)} * @throws IllegalArgumentException if {@code key} is null */ public static int writeLength(byte[] key, byte[] prev) { Preconditions.checkArgument(key != null, "null key"); int prefixLength = 0; if (prev != null) { while (prefixLength < key.length && prefixLength < prev.length && key[prefixLength] == prev[prefixLength]) prefixLength++; } if (prefixLength > 1) { final int suffixLength = key.length - prefixLength; return LongEncoder.encodeLength(~(prefixLength - 2)) + UnsignedIntEncoder.encodeLength(suffixLength) + suffixLength; } else return LongEncoder.encodeLength(key.length) + key.length; } /** * Read the next key. * * @param input input stream * @param prev previous key, or null for none * @return next key * @throws IOException if an I/O error occurs * @throws EOFException if an unexpected EOF is encountered * @throws IllegalArgumentException if {@code input} is null * @throws IllegalArgumentException if {@code input} contains invalid data */ public static byte[] read(InputStream input, byte[] prev) throws IOException { Preconditions.checkArgument(input != null, "null input"); // Get encoded length of prefix int keyLength = KeyListEncoder.readSignedInt(input); final byte[] key; int prefixLength; // Decode prefix length and read prefix, if any if (keyLength < 0) { if (prev == null) throw new IllegalArgumentException("null `prev' given but next key has " + -keyLength + " byte shared prefix"); prefixLength = ~keyLength + 2; final int suffixLength = UnsignedIntEncoder.read(input); keyLength = prefixLength + suffixLength; if (keyLength < 0) throw new IllegalArgumentException("invalid prefix length " + prefixLength + " plus suffix length " + suffixLength); key = new byte[keyLength]; System.arraycopy(prev, 0, key, 0, prefixLength); } else { key = new byte[keyLength]; prefixLength = 0; } // Read suffix while (prefixLength < key.length) { final int num = input.read(key, prefixLength, key.length - prefixLength); if (num == -1) throw new EOFException(); prefixLength += num; } // Done return key; } /** * Encode an iteration of key/value pairs. * * @param kvpairs key/value pair iteration * @param output encoded output * @throws IOException if an I/O error occurs * @throws IllegalArgumentException if either parameter is null */ public static void writePairs(Iterator kvpairs, OutputStream output) throws IOException { // Sanity check Preconditions.checkArgument(kvpairs != null, "null kvpairs"); Preconditions.checkArgument(output != null, "null output"); // Write pairs byte[] prev = null; while (kvpairs.hasNext()) { final KVPair kv = kvpairs.next(); final byte[] key = kv.getKey(); final byte[] value = kv.getValue(); KeyListEncoder.write(output, key, prev); KeyListEncoder.write(output, value, null); prev = key; } // Write special terminator byte output.write(0xff); } /** * Determine the number of bytes that would be written by {@link #writePairs writePairs()}. * * @param kvpairs key/value pair iteration * @return encoded length of this instance * @throws IllegalArgumentException if {@code kvpairs} is null */ public static long writePairsLength(Iterator kvpairs) { long total = 1; byte[] prev = null; while (kvpairs.hasNext()) { final KVPair kv = kvpairs.next(); final byte[] key = kv.getKey(); final byte[] value = kv.getValue(); total += KeyListEncoder.writeLength(key, prev); total += KeyListEncoder.writeLength(value, null); prev = key; } return total; } /** * Decode an iteration of key/value pairs previously encoded by {@link #writePairs writePairs()}. * *

* If an {@link IOException} occurs during iteration, the returned {@link Iterator} wraps it in a {@link RuntimeException}. * *

* If invalid input is encountered during iteration, the returned {@link Iterator} * will throw an {@link IllegalArgumentException}. * * @param input encoded input * @return iteration of key/value pairs * @throws IllegalArgumentException if {@code input} is null */ public static Iterator readPairs(final InputStream input) { // Sanity check Preconditions.checkArgument(input != null, "null input"); // Decode return new AbstractIterator() { private final BufferedInputStream in = new BufferedInputStream(input, 1024); private byte[] prev; @Override protected KVPair computeNext() { try { // Check for terminator this.in.mark(1); final int b = in.read(); if (b == -1) throw new EOFException("truncated input"); if (b == 0xff) return endOfData(); this.in.reset(); // Read next k/v pair final byte[] key = KeyListEncoder.read(this.in, this.prev); final byte[] value = KeyListEncoder.read(this.in, null); this.prev = key; // Done return new KVPair(key, value); } catch (IOException e) { throw new RuntimeException("I/O error during iteration", e); } } }; } private static int readSignedInt(InputStream input) throws IOException { final long longValue = LongEncoder.read(input); final int intValue = (int)longValue; Preconditions.checkArgument(intValue == (int)longValue, "read out-of-range encoded int value %s", longValue); return intValue; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy