package io.eguan.dtx.journal;
/*
* #%L
* Project eguan
* %%
* Copyright (C) 2012 - 2017 Oodrive
* %%
* 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.
* #L%
*/
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.ReadableByteChannel;
import java.util.Arrays;
import java.util.zip.CRC32;
import javax.annotation.Nonnull;
import javax.annotation.concurrent.Immutable;
/**
* An {@link Immutable} journal record including a length and checksum field.
*
* Journal entries are handled transparently as byte arrays without any parsing or validation to avoid useless overhead
* while reading records sequentially.
*
* <table border='1'>
* <tr>
* <td>length (int)</td>
* <td>content (bytes)</td>
* <td>CRC32 checksum (long)</td>
* </tr>
* <tr>
* <td>the length in bytes of the entry</td>
* <td>the binary journal entry of the record, at least 1 byte long</td>
* <td>a {@link CRC32} checksum computed on the length and entry fields</td>
* </tr>
* </table>
*
* @author oodrive
* @author pwehrle
*
*/
@Immutable
public final class JournalRecord {
/**
* Maximum length of an entry in bytes.
*/
public static final long MAX_RECORD_LENGTH_BYTES = 20971520L; // = 10 MB
// long and integer sizes in bytes
private static final int LONG_SIZE_BYTES = Long.SIZE / Byte.SIZE;
private static final int INT_SIZE_BYTES = Integer.SIZE / Byte.SIZE;
/**
* The journal entry.
*/
private final byte[] entry;
/**
* The {@link CRC32} checksum computed on length and entry fields.
*/
private final long checksum;
/**
* The complete binary content of this record.
*/
private final byte[] content;
/**
* Constructs an instance with the given data as entry.
*
* @param entry
* the non-empty entry data to include in the
* @throws IllegalArgumentException
* if the argument is empty
* @throws NullPointerException
* if the argument is <code>null</code>
*/
JournalRecord(final byte[] entry) throws IllegalArgumentException, NullPointerException {
if (entry.length == 0) {
throw new IllegalArgumentException("Entry must not be empty");
}
this.entry = Arrays.copyOf(entry, entry.length);
// initializes the content array
content = new byte[this.entry.length + INT_SIZE_BYTES + LONG_SIZE_BYTES];
final ByteBuffer buffer = ByteBuffer.wrap(content);
// writes the length field and the entry
buffer.putInt(entry.length);
buffer.put(entry);
// computes and writes the checksum
final CRC32 chksum = new CRC32();
chksum.update(content, 0, this.entry.length + INT_SIZE_BYTES);
checksum = chksum.getValue();
buffer.putLong(checksum);
}
/**
* Private constructor for internal use without any argument validation.
*
* @param entry
* @param checksum
* @param content
*/
private JournalRecord(final byte[] entry, final long checksum, final byte[] content) {
this.entry = entry;
this.checksum = checksum;
this.content = content;
}
/**
* Gets the journal entry.
*
* @return a copy of the entry as given at construction time
*/
public final byte[] getEntry() {
return Arrays.copyOf(entry, entry.length);
}
/**
* Gets the record's checksum.
*
* @return the checksum as provided by {@link CRC32#getValue()}
*/
final long getChecksum() {
return checksum;
}
/**
* Gets the binary content of this record.
*
* @return a copy of the complete content of this record
*/
final byte[] getContent() {
return Arrays.copyOf(content, content.length);
}
/**
* Builds a new {@link JournalRecord} from binary content.
*
* This method expects to find the length and checksum field and an entry at least 1 byte long.
*
* @param content
* the binary content from which to read the record
* @return a functional {@link JournalRecord}
* @throws IllegalArgumentException
* if the record is too short
*/
static JournalRecord buildJournalRecord(final byte[] content) throws IllegalArgumentException {
if (content.length < INT_SIZE_BYTES + LONG_SIZE_BYTES + 1) {
throw new IllegalArgumentException("Record content much too short");
}
final byte[] newContent = Arrays.copyOf(content, content.length);
// reads the entry length
final ByteBuffer buffer = ByteBuffer.wrap(newContent);
final int entryLength = buffer.getInt();
final int expLength = entryLength + LONG_SIZE_BYTES;
if (buffer.remaining() < expLength) {
throw new IllegalArgumentException("Input provides to little data; expected=" + expLength + ",found="
+ buffer.remaining());
}
// reads the entry itself
final byte[] newEntry = new byte[entryLength];
buffer.get(newEntry);
// computes the checksum
final CRC32 chksum = new CRC32();
final int checkBytes = INT_SIZE_BYTES + entryLength;
chksum.update(newContent, 0, checkBytes);
// reads and verifies the checksum
final long verifSum = buffer.getLong(checkBytes);
if (verifSum != chksum.getValue()) {
throw new IllegalArgumentException("Checksum verification failed");
}
return new JournalRecord(newEntry, verifSum, newContent);
}
/**
* Reads a journal record from the given {@link ReadableByteChannel}.
*
* Unlike {@link #buildJournalRecord(byte[])}, this method returns <code>null</code> if it finds there is not enough
* data to read an entire record to accommodate the use by iterators.
*
* @param input
* a non-<code>null</code> {@link ReadableByteChannel}
* @return a valid {@link JournalRecord} or <code>null</code> if there was not enough data
* @throws IOException
* if reading from the input fails
* @throws NullPointerException
* if the argument is <code>null</code>
* @throws IllegalArgumentException
* if the input contains invalid data
*/
static JournalRecord readRecordFromByteChannel(@Nonnull final ReadableByteChannel input) throws IOException,
NullPointerException, IllegalArgumentException {
final byte[] lengthField = new byte[INT_SIZE_BYTES];
final ByteBuffer lengthBuffer = ByteBuffer.wrap(lengthField);
if (input.read(lengthBuffer) < INT_SIZE_BYTES) {
return null;
}
final int entryLength = lengthBuffer.getInt(0);
if (entryLength <= 0) {
throw new IllegalArgumentException("Invalid entry length; entryLength=" + entryLength);
}
if (entryLength >= MAX_RECORD_LENGTH_BYTES) {
throw new IllegalArgumentException("Entry length larger than maximum allowable size; entryLength="
+ entryLength + ", maximum size=" + MAX_RECORD_LENGTH_BYTES);
}
final int expRestLength = entryLength + LONG_SIZE_BYTES;
final byte[] newContent = Arrays.copyOf(lengthField, INT_SIZE_BYTES + expRestLength);
final ByteBuffer newContentBuffer = ByteBuffer.wrap(newContent);
newContentBuffer.position(lengthField.length);
if (input.read(newContentBuffer) < expRestLength) {
return null;
}
return buildJournalRecord(newContent);
}
}