/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF 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 org.apache.jackrabbit.core.journal;
import org.apache.commons.io.IOUtils;
import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver;
import org.apache.jackrabbit.spi.commons.namespace.NamespaceResolver;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.BufferedInputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
/**
* A file record log is a file containing {@link Record}s. Every file record
* log contains a header with the following physical layout:
*
* <blockquote>
* <table border="2" cellpadding="4" summary="Physical Record Layout">
* <tr align="center" valign="bottom" bgcolor="silver">
* <td><tt>Byte 1</tt></td>
* <td><tt>Byte 2</tt></td>
* <td><tt>Byte 3</tt></td>
* <td><tt>Byte 4</tt></td>
* </tr>
* <tr>
* <td align="center"><tt>'J'</tt></td>
* <td align="center"><tt>'L'</tt></td>
* <td align="center"><tt>'O'</tt></td>
* <td align="center"><tt>'G'</tt></td>
* </tr>
* <tr>
* <td align="center" colspan="2"><tt>MAJOR</tt></td>
* <td align="center" colspan="2"><tt>MINOR</tt></td>
* </tr>
* <tr>
* <td align="center" colspan="4"><tt>START REVISION</tt></td>
* </tr>
* </table>
* </blockquote>
*
* After this header, zero or more <code>ReadRecord</code>s follow.
*/
public class FileRecordLog {
/**
* Logger.
*/
private static Logger log = LoggerFactory.getLogger(FileRecordLog.class);
/**
* Record log signature.
*/
private static final byte[] SIGNATURE = { 'J', 'L', 'O', 'G' };
/**
* Known major version.
*/
private static final short MAJOR_VERSION = 2;
/**
* Known minor version.
*/
private static final short MINOR_VERSION = 0;
/**
* Header size. This is the size of {@link #SIGNATURE}, {@link #MAJOR_VERSION},
* {@link #MINOR_VERSION} and first revision (8 bytes).
*/
private static final int HEADER_SIZE = 4 + 2 + 2 + 8;
/**
* Underlying file.
*/
private File logFile;
/**
* Flag indicating whether this is a new log.
*/
private boolean isNew;
/**
* Input stream used when seeking a specific record.
*/
private DataInputStream in;
/**
* Last revision that is not in this log.
*/
private long previousRevision;
/**
* Relative position inside this log.
*/
private long position;
/**
* Last revision that is available in this log.
*/
private long lastRevision;
/**
* Major version found in record log.
*/
private short major;
/**
* Minor version found in record log.
*/
private short minor;
/**
* Create a new instance of this class. Opens a record log in read-only mode.
*
* @param logFile file containing record log
* @throws java.io.IOException if an I/O error occurs
*/
public FileRecordLog(File logFile) throws IOException {
this.logFile = logFile;
if (logFile.exists()) {
DataInputStream in = new DataInputStream(
new BufferedInputStream(new FileInputStream(logFile), 128));
try {
readHeader(in);
previousRevision = in.readLong();
lastRevision = previousRevision + logFile.length() - HEADER_SIZE;
} finally {
close(in);
}
} else {
isNew = true;
}
}
/**
* Initialize this record log by writing a header containing the
* previous revision.
*/
public void init(long previousRevision) throws IOException {
if (isNew) {
DataOutputStream out = new DataOutputStream(
new BufferedOutputStream(new FileOutputStream(logFile), 128));
try {
writeHeader(out);
out.writeLong(previousRevision);
} finally {
close(out);
}
this.previousRevision = previousRevision;
this.lastRevision = previousRevision;
isNew = false;
}
}
/**
* Return a flag indicating whether this record log contains a certain revision.
*
* @param revision revision to look for
* @return <code>true</code> if this record log contain a certain revision;
* <code>false</code> otherwise
*/
public boolean contains(long revision) {
return (revision >= previousRevision && revision < lastRevision);
}
/**
* Return a flag indicating whether this record log is new.
*
* @return <code>true</code> if this record log is new;
* <code>false</code> otherwise
*/
public boolean isNew() {
return isNew;
}
/**
* Return a flag indicating whether this record log exceeds a given size.
*/
public boolean exceeds(long size) {
return (lastRevision - previousRevision) > size;
}
/**
* Seek an entry. This is an operation that allows the underlying input stream
* to be sequentially scanned and must therefore not be called twice.
*
* @param revision revision to seek
* @throws java.io.IOException if an I/O error occurs
*/
public void seek(long revision) throws IOException {
if (in != null) {
String msg = "Stream already open: seek() only allowed once.";
throw new IllegalStateException(msg);
}
in = new DataInputStream(new BufferedInputStream(
new FileInputStream(logFile)));
skip(revision - previousRevision + HEADER_SIZE);
position = revision - previousRevision;
}
/**
* Skip exactly <code>n</code> bytes. Throws if less bytes are skipped.
*
* @param n bytes to skip
* @throws java.io.IOException if an I/O error occurs, or less that <code>n</code> bytes
* were skipped.
*/
private void skip(long n) throws IOException {
long skiplen = n;
while (skiplen > 0) {
long skipped = in.skip(skiplen);
if (skipped <= 0) {
break;
}
skiplen -= skipped;
}
if (skiplen != 0) {
String msg = "Unable to skip remaining bytes.";
throw new IOException(msg);
}
}
/**
* Read the file record at the current seek position.
*
* @param resolver namespace resolver
* @return file record
* @throws java.io.IOException if an I/O error occurs
*/
public ReadRecord read(NamespaceResolver resolver, NamePathResolver npResolver) throws IOException {
String journalId = in.readUTF();
String producerId = in.readUTF();
int length = in.readInt();
position +=
2 + utfLength(journalId) + 2 + utfLength(producerId) + 4 + length;
long revision = previousRevision + position;
return new ReadRecord(journalId, producerId, revision, in, length, resolver, npResolver);
}
/**
* Append a record to this log. Returns the revision following this record.
*
* @param journalId journal identifier
* @param producerId producer identifier
* @param in record to add
* @param length record length
* @throws java.io.IOException if an I/O error occurs
*/
public long append(String journalId, String producerId, InputStream in, int length)
throws IOException {
OutputStream out = new FileOutputStream(logFile, true);
try {
DataBuffer buffer = new DataBuffer();
buffer.writeUTF(journalId);
buffer.writeUTF(producerId);
buffer.writeInt(length);
buffer.copy(out);
IOUtils.copy(in, out);
out.flush();
lastRevision +=
2 + utfLength(journalId) + 2 + utfLength(producerId)
+ 4 + length;
return lastRevision;
} finally {
close(out);
}
}
/**
* Return the previous revision. This is the last revision preceding the
* first revision in this log.
*
* @return previous revision
*/
public long getPreviousRevision() {
return previousRevision;
}
/**
* Return the last revision. This is the last revision in this log.
*
* @return last revision
*/
public long getLastRevision() {
return lastRevision;
}
/**
* Close this log.
*/
public void close() {
try {
if (in != null) {
in.close();
}
} catch (IOException e) {
String msg = "Error while closing record log: " + e.getMessage();
log.warn(msg);
}
}
/**
* Read signature and major/minor version of file and verify.
*
* @param in input stream
* @throws java.io.IOException if an I/O error occurs or the file does
* not have a valid header.
*/
private void readHeader(DataInputStream in) throws IOException {
byte[] signature = new byte[SIGNATURE.length];
in.readFully(signature);
for (int i = 0; i < SIGNATURE.length; i++) {
if (signature[i] != SIGNATURE[i]) {
String msg = "Record log '" + logFile.getPath()
+ "' has wrong signature: " + toHexString(signature);
throw new IOException(msg);
}
}
major = in.readShort();
if (major != MAJOR_VERSION) {
String msg = "Record log '" + logFile.getPath()
+ "' has incompatible major version: " + major;
throw new IOException(msg);
}
minor = in.readShort();
}
/**
* Write signature and major/minor.
*
* @param out input stream
* @throws java.io.IOException if an I/O error occurs.
*/
private void writeHeader(DataOutputStream out) throws IOException {
out.write(SIGNATURE);
out.writeShort(MAJOR_VERSION);
out.writeShort(MINOR_VERSION);
}
/**
* Close an input stream, logging a warning if an error occurs.
*/
private static void close(InputStream in) {
try {
in.close();
} catch (IOException e) {
String msg = "I/O error while closing input stream.";
log.warn(msg, e);
}
}
/**
* Close an output stream, logging a warning if an error occurs.
*/
private static void close(OutputStream out) {
try {
out.close();
} catch (IOException e) {
String msg = "I/O error while closing input stream.";
log.warn(msg, e);
}
}
/**
* Convert a byte array to its hexadecimal string representation.
*/
private static String toHexString(byte[] b) {
StringBuilder buf = new StringBuilder();
for (int i = 0; i < b.length; i++) {
String s = Integer.toHexString(b[i] & 0xff).toUpperCase();
if (s.length() == 1) {
buf.append('0');
}
buf.append(s);
}
return buf.toString();
}
/**
* Return the length of a string when converted to its Java modified
* UTF-8 encoding, as used by <code>DataInput.readUTF</code> and
* <code>DataOutput.writeUTF</code>.
*/
private static int utfLength(String s) {
char[] ac = s.toCharArray();
int utflen = 0;
for (int i = 0; i < ac.length; i++) {
char c = ac[i];
if ((c >= 0x0001) && (c <= 0x007F)) {
utflen++;
} else if (c > 0x07FF) {
utflen += 3;
} else {
utflen += 2;
}
}
return utflen;
}
/**
* A simple helper class that writes to a buffer. The current buffer can
* be {@link #copy copied} to an output stream.
*/
private static final class DataBuffer extends DataOutputStream {
public DataBuffer() {
super(new ByteArrayOutputStream());
}
/**
* Copies the bytes the are currently held in the buffer to the given
* output stream.
*
* @param out the output stream where the buffered data is written.
* @throws IOException if an error occurs while writing data to
* <code>out</code>.
*/
public void copy(OutputStream out) throws IOException {
byte[] buffer = ((ByteArrayOutputStream) super.out).toByteArray();
out.write(buffer);
}
}
}