c5db.log.EntryEncodingUtil.java Source code

Java tutorial

Introduction

Here is the source code for c5db.log.EntryEncodingUtil.java

Source

/*
 * Copyright 2014 WANdisco
 *
 *  WANdisco licenses this file to you 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 c5db.log;

import c5db.util.CrcInputStream;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.primitives.Ints;
import io.protostuff.LinkBuffer;
import io.protostuff.LowCopyProtobufOutput;
import io.protostuff.ProtobufIOUtil;
import io.protostuff.Schema;

import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.util.Collection;
import java.util.List;
import java.util.zip.Adler32;

import static com.google.common.math.IntMath.checkedAdd;

/**
 * Contains methods used for encoding and decoding log entries
 */
public class EntryEncodingUtil {

    /**
     * Exception indicating that a CRC has been read which does not match up with
     * the CRC computed from the associated data.
     */
    public static class CrcError extends RuntimeException {
        public CrcError(String s) {
            super(s);
        }
    }

    /**
     * Serialize a protostuff message object, prefixed with message length, and suffixed with a 4-byte CRC.
     *
     * @param schema  Protostuff message schema
     * @param message Object to serialize
     * @param <T>     Message type
     * @return A list of ByteBuffers containing a varInt length, followed by the message, followed by a 4-byte CRC.
     */
    public static <T> List<ByteBuffer> encodeWithLengthAndCrc(Schema<T> schema, T message) {
        final LinkBuffer messageBuf = new LinkBuffer();
        final LowCopyProtobufOutput lcpo = new LowCopyProtobufOutput(messageBuf);

        try {
            schema.writeTo(lcpo, message);

            final int length = Ints.checkedCast(lcpo.buffer.size());
            final LinkBuffer lengthBuf = new LinkBuffer().writeVarInt32(length);

            return appendCrcToBufferList(
                    Lists.newArrayList(Iterables.concat(lengthBuf.finish(), messageBuf.finish())));
        } catch (IOException e) {
            // This method performs no IO, so it should not actually be possible for an IOException to be thrown.
            // But just in case...
            throw new RuntimeException(e);
        }
    }

    /**
     * Decode a message from the passed input stream, and compute and verify its CRC. This method reads
     * data written by the method {@link EntryEncodingUtil#encodeWithLengthAndCrc}.
     *
     * @param inputStream Input stream, opened for reading and positioned just before the length-prepended header
     * @return The deserialized, constructed, validated message
     * @throws IOException                if a problem is encountered while reading or parsing
     * @throws EntryEncodingUtil.CrcError if the recorded CRC of the message does not match its computed CRC.
     */
    public static <T> T decodeAndCheckCrc(InputStream inputStream, Schema<T> schema) throws IOException, CrcError {
        // TODO this should check the length first and compare it with a passed-in maximum length
        final T message = schema.newMessage();
        final CrcInputStream crcStream = new CrcInputStream(inputStream, new Adler32());
        ProtobufIOUtil.mergeDelimitedFrom(crcStream, message, schema);

        final long computedCrc = crcStream.getValue();
        final long diskCrc = readCrc(inputStream);
        if (diskCrc != computedCrc) {
            throw new CrcError("CRC mismatch on deserialized message " + message.toString());
        }

        return message;
    }

    /**
     * Given a list of ByteBuffers, compute the combined CRC and then append it to the list as one or more
     * additional ByteBuffers. Return the entire resulting collection as a new list, including the original
     * ByteBuffers.
     *
     * @param content non-null list of ByteBuffers; no mutation will be performed on them.
     * @return New list of ByteBuffers, with the CRC appended to the original ByteBuffers
     */
    public static List<ByteBuffer> appendCrcToBufferList(List<ByteBuffer> content) throws IOException {
        assert content != null;

        final Adler32 crc = new Adler32();
        content.forEach((ByteBuffer buffer) -> crc.update(buffer.duplicate()));
        final LinkBuffer crcBuf = new LinkBuffer(8);
        putCrc(crcBuf, crc.getValue());

        return Lists.newArrayList(Iterables.concat(content, crcBuf.finish()));
    }

    /**
     * Write a passed CRC to the passed buffer. The CRC is a 4-byte unsigned integer stored in a long; write it
     * as (fixed length) 4 bytes.
     *
     * @param writeTo Buffer to write to; exactly 4 bytes will be written.
     * @param crc     CRC to write; caller guarantees that the code is within the range:
     *                0 <= CRC < 2^32
     */
    private static void putCrc(final LinkBuffer writeTo, final long crc) throws IOException {
        // To store the CRC in an int, we need to subtract to convert it from unsigned to signed.
        final long shiftedCrc = crc + Integer.MIN_VALUE;
        writeTo.writeInt32(Ints.checkedCast(shiftedCrc));
    }

    private static long readCrc(InputStream inputStream) throws IOException {
        int shiftedCrc = (new DataInputStream(inputStream)).readInt();
        return ((long) shiftedCrc) - Integer.MIN_VALUE;
    }

    /**
     * Read a specified number of bytes from the input stream (the "content"), then read one or more CRC codes and
     * check the validity of the data.
     *
     * @param inputStream   Input stream, opened for reading and positioned just before the content
     * @param contentLength Length of data to read from inputStream, not including any trailing CRCs
     * @return The read content, as a ByteBuffer.
     * @throws IOException
     */
    public static ByteBuffer getAndCheckContent(InputStream inputStream, int contentLength)
            throws IOException, CrcError {
        // TODO probably not the correct way to do this... should use IOUtils?
        final CrcInputStream crcStream = new CrcInputStream(inputStream, new Adler32());
        final byte[] content = new byte[contentLength];
        final int len = crcStream.read(content);

        if (len < contentLength) {
            // Data wasn't available that we expected to be
            throw new IllegalStateException("Reading a log entry's contents returned fewer than expected bytes");
        }

        final long computedCrc = crcStream.getValue();
        final long diskCrc = readCrc(inputStream);
        if (diskCrc != computedCrc) {
            throw new CrcError("CRC mismatch on log entry contents");
        }

        return ByteBuffer.wrap(content);
    }

    public static void skip(InputStream inputStream, int numBytes) throws IOException {
        long actuallySkipped = inputStream.skip(numBytes);
        if (actuallySkipped < numBytes) {
            throw new IOException("Unable to skip requested number of bytes");
        }
    }

    /**
     * Add up the lengths of the content of each buffer in the passed list, and return the sum of the lengths.
     *
     * @param buffers List of buffers; this method will not mutate them.
     * @return The sum of the remaining() bytes in each buffer.
     */
    public static int sumRemaining(Collection<ByteBuffer> buffers) {
        int length = 0;
        if (buffers != null) {
            for (ByteBuffer b : buffers) {
                length = checkedAdd(length, b.remaining());
            }
        }
        return length;
    }
}