blob: e4d493eef1d8a81c9a7593d12cba2ddf044043a0 [file] [log] [blame]
/* Copyright (C) 2003 Vladimir Roubtsov. All rights reserved.
*
* This program and the accompanying materials are made available under
* the terms of the Common Public License v1.0 which accompanies this distribution,
* and is available at http://www.eclipse.org/legal/cpl-v10.html
*
* $Id: DataFactory.java,v 1.1.1.1.2.3 2004/07/16 23:32:29 vlad_r Exp $
*/
package com.vladium.emma.data;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.DataInput;
import java.io.DataInputStream;
import java.io.DataOutput;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.net.URL;
import java.net.URLConnection;
import com.vladium.logging.Logger;
import com.vladium.util.asserts.$assert;
import com.vladium.emma.IAppConstants;
// ----------------------------------------------------------------------------
/**
* @author Vlad Roubtsov, (C) 2003
*/
public
abstract class DataFactory
{
// public: ................................................................
// TODO: file compaction
// TODO: file locking
// TODO: what's the best place for these?
public static final byte TYPE_METADATA = 0x0; // must start with 0
public static final byte TYPE_COVERAGEDATA = 0x1; // must be consistent with mergeload()
public static IMergeable [] load (final File file)
throws IOException
{
if (file == null) throw new IllegalArgumentException ("null input: file");
return mergeload (file);
}
public static void persist (final IMetaData data, final File file, final boolean merge)
throws IOException
{
if (data == null) throw new IllegalArgumentException ("null input: data");
if (file == null) throw new IllegalArgumentException ("null input: file");
if (! merge && file.exists ())
{
if (! file.delete ())
throw new IOException ("could not delete file [" + file.getAbsolutePath () + "]");
}
persist (data, TYPE_METADATA, file);
}
public static void persist (final ICoverageData data, final File file, final boolean merge)
throws IOException
{
if (data == null) throw new IllegalArgumentException ("null input: data");
if (file == null) throw new IllegalArgumentException ("null input: file");
if (! merge && file.exists ())
{
if (! file.delete ())
throw new IOException ("could not delete file [" + file.getAbsolutePath () + "]");
}
persist (data, TYPE_COVERAGEDATA, file);
}
public static void persist (final ISessionData data, final File file, final boolean merge)
throws IOException
{
if (data == null) throw new IllegalArgumentException ("null input: data");
if (file == null) throw new IllegalArgumentException ("null input: file");
if (! merge && file.exists ())
{
if (! file.delete ())
throw new IOException ("could not delete file [" + file.getAbsolutePath () + "]");
}
persist (data.getMetaData (), TYPE_METADATA, file);
persist (data.getCoverageData (), TYPE_COVERAGEDATA, file);
}
public static IMetaData newMetaData (final CoverageOptions options)
{
return new MetaData (options);
}
public static ICoverageData newCoverageData ()
{
return new CoverageData ();
}
public static IMetaData readMetaData (final URL url)
throws IOException, ClassNotFoundException
{
ObjectInputStream oin = null;
try
{
oin = new ObjectInputStream (new BufferedInputStream (url.openStream (), 32 * 1024));
return (IMetaData) oin.readObject ();
}
finally
{
if (oin != null) try { oin.close (); } catch (Exception ignore) {}
}
}
public static void writeMetaData (final IMetaData data, final OutputStream out)
throws IOException
{
ObjectOutputStream oout = new ObjectOutputStream (out);
oout.writeObject (data);
}
public static void writeMetaData (final IMetaData data, final URL url)
throws IOException
{
final URLConnection connection = url.openConnection ();
connection.setDoOutput (true);
OutputStream out = null;
try
{
out = connection.getOutputStream ();
writeMetaData (data, out);
out.flush ();
}
finally
{
if (out != null) try { out.close (); } catch (Exception ignore) {}
}
}
public static ICoverageData readCoverageData (final URL url)
throws IOException, ClassNotFoundException
{
ObjectInputStream oin = null;
try
{
oin = new ObjectInputStream (new BufferedInputStream (url.openStream (), 32 * 1024));
return (ICoverageData) oin.readObject ();
}
finally
{
if (oin != null) try { oin.close (); } catch (Exception ignore) {}
}
}
public static void writeCoverageData (final ICoverageData data, final OutputStream out)
throws IOException
{
// TODO: prevent concurrent modification problems here
ObjectOutputStream oout = new ObjectOutputStream (out);
oout.writeObject (data);
}
public static int [] readIntArray (final DataInput in)
throws IOException
{
final int length = in.readInt ();
if (length == NULL_ARRAY_LENGTH)
return null;
else
{
final int [] result = new int [length];
// read array in reverse order:
for (int i = length; -- i >= 0; )
{
result [i] = in.readInt ();
}
return result;
}
}
public static boolean [] readBooleanArray (final DataInput in)
throws IOException
{
final int length = in.readInt ();
if (length == NULL_ARRAY_LENGTH)
return null;
else
{
final boolean [] result = new boolean [length];
// read array in reverse order:
for (int i = length; -- i >= 0; )
{
result [i] = in.readBoolean ();
}
return result;
}
}
public static void writeIntArray (final int [] array, final DataOutput out)
throws IOException
{
if (array == null)
out.writeInt (NULL_ARRAY_LENGTH);
else
{
final int length = array.length;
out.writeInt (length);
// write array in reverse order:
for (int i = length; -- i >= 0; )
{
out.writeInt (array [i]);
}
}
}
public static void writeBooleanArray (final boolean [] array, final DataOutput out)
throws IOException
{
if (array == null)
out.writeInt (NULL_ARRAY_LENGTH);
else
{
final int length = array.length;
out.writeInt (length);
// write array in reverse order:
for (int i = length; -- i >= 0; )
{
out.writeBoolean (array [i]);
}
}
}
// protected: .............................................................
// package: ...............................................................
// private: ...............................................................
private static final class UCFileInputStream extends FileInputStream
{
public void close ()
{
}
UCFileInputStream (final FileDescriptor fd)
{
super (fd);
if ($assert.ENABLED) $assert.ASSERT (fd.valid (), "UCFileInputStream.<init>: FD invalid");
}
} // end of nested class
private static final class UCFileOutputStream extends FileOutputStream
{
public void close ()
{
}
UCFileOutputStream (final FileDescriptor fd)
{
super (fd);
if ($assert.ENABLED) $assert.ASSERT (fd.valid (), "UCFileOutputStream.<init>: FD invalid");
}
} // end of nested class
private static final class RandomAccessFileInputStream extends BufferedInputStream
{
public final int read () throws IOException
{
final int rc = super.read ();
if (rc >= 0) ++ m_count;
return rc;
}
public final int read (final byte [] b, final int off, final int len)
throws IOException
{
final int rc = super.read (b, off, len);
if (rc >= 0) m_count += rc;
return rc;
}
public final int read (final byte [] b) throws IOException
{
final int rc = super.read (b);
if (rc >= 0) m_count += rc;
return rc;
}
public void close ()
{
}
RandomAccessFileInputStream (final RandomAccessFile raf, final int bufSize)
throws IOException
{
super (new UCFileInputStream (raf.getFD ()), bufSize);
}
final long getCount ()
{
return m_count;
}
private long m_count;
} // end of nested class
private static final class RandomAccessFileOutputStream extends BufferedOutputStream
{
public final void write (final byte [] b, final int off, final int len) throws IOException
{
super.write (b, off, len);
m_count += len;
}
public final void write (final byte [] b) throws IOException
{
super.write (b);
m_count += b.length;
}
public final void write (final int b) throws IOException
{
super.write (b);
++ m_count;
}
public void close ()
{
}
RandomAccessFileOutputStream (final RandomAccessFile raf, final int bufSize)
throws IOException
{
super (new UCFileOutputStream (raf.getFD ()), bufSize);
}
final long getCount ()
{
return m_count;
}
private long m_count;
} // end of nested class
private DataFactory () {} // prevent subclassing
/*
* input checked by the caller
*/
private static IMergeable [] mergeload (final File file)
throws IOException
{
final Logger log = Logger.getLogger ();
final boolean trace1 = log.atTRACE1 ();
final boolean trace2 = log.atTRACE2 ();
final String method = "mergeload";
long start = 0, end;
if (trace1) start = System.currentTimeMillis ();
final IMergeable [] result = new IMergeable [2];
if (! file.exists ())
{
throw new IOException ("input file does not exist: [" + file.getAbsolutePath () + "]");
}
else
{
RandomAccessFile raf = null;
try
{
raf = new RandomAccessFile (file, "r");
// 'file' is a valid existing file, but it could still be of 0 length or otherwise corrupt:
final long length = raf.length ();
if (trace1) log.trace1 (method, "[" + file + "]: file length = " + length);
if (length < FILE_HEADER_LENGTH)
{
throw new IOException ("file [" + file.getAbsolutePath () + "] is corrupt or was not created by " + IAppConstants.APP_NAME);
}
else
{
// TODO: data version checks parallel to persist()
if (length > FILE_HEADER_LENGTH) // return {null, null} in case of equality
{
raf.seek (FILE_HEADER_LENGTH);
// [assertion: file length > FILE_HEADER_LENGTH]
// read entries until the first corrupt entry or the end of the file:
long position = FILE_HEADER_LENGTH;
long entryLength;
long entrystart = 0;
while (true)
{
if (trace2) log.trace2 (method, "[" + file + "]: position " + raf.getFilePointer ());
if (position >= length) break;
entryLength = raf.readLong ();
if ((entryLength <= 0) || (position + entryLength + ENTRY_HEADER_LENGTH > length))
break;
else
{
final byte type = raf.readByte ();
if ((type < 0) || (type >= result.length))
break;
if (trace2) log.trace2 (method, "[" + file + "]: found valid entry of size " + entryLength + " and type " + type);
{
if (trace2) entrystart = System.currentTimeMillis ();
final IMergeable data = readEntry (raf, type, entryLength);
if (trace2) log.trace2 (method, "entry read in " + (System.currentTimeMillis () - entrystart) + " ms");
final IMergeable current = result [type];
if (current == null)
result [type] = data;
else
result [type] = current.merge (data); // note: later entries overrides earlier entries
}
position += entryLength + ENTRY_HEADER_LENGTH;
if ($assert.ENABLED) $assert.ASSERT (raf.getFD ().valid (), "FD invalid");
raf.seek (position);
}
}
}
}
}
finally
{
if (raf != null) try { raf.close (); } catch (Throwable ignore) {}
raf = null;
}
}
if (trace1)
{
end = System.currentTimeMillis ();
log.trace1 (method, "[" + file + "]: file processed in " + (end - start) + " ms");
}
return result;
}
/*
* input checked by the caller
*/
private static void persist (final IMergeable data, final byte type, final File file)
throws IOException
{
final Logger log = Logger.getLogger ();
final boolean trace1 = log.atTRACE1 ();
final boolean trace2 = log.atTRACE2 ();
final String method = "persist";
long start = 0, end;
if (trace1) start = System.currentTimeMillis ();
// TODO: 1.4 adds some interesting RAF open mode options as well
// TODO: will this benefit from extra buffering?
// TODO: data version checks
RandomAccessFile raf = null;
try
{
boolean overwrite = false;
boolean truncate = false;
if (file.exists ())
{
// 'file' exists:
if (! file.isFile ()) throw new IOException ("can persist in normal files only: " + file.getAbsolutePath ());
raf = new RandomAccessFile (file, "rw");
// 'file' is a valid existing file, but it could still be of 0 length or otherwise corrupt:
final long length = raf.length ();
if (trace1) log.trace1 (method, "[" + file + "]: existing file length = " + length);
if (length < 4)
{
overwrite = true;
truncate = (length > 0);
}
else
{
// [assertion: file length >= 4]
// check header info before reading further:
final int magic = raf.readInt ();
if (magic != MAGIC)
throw new IOException ("cannot overwrite [" + file.getAbsolutePath () + "]: not created by " + IAppConstants.APP_NAME);
if (length < FILE_HEADER_LENGTH)
{
// it's our file, but the header is corrupt: overwrite
overwrite = true;
truncate = true;
}
else
{
// [assertion: file length >= FILE_HEADER_LENGTH]
// if (! append)
// {
// // overwrite any existing data:
//
// raf.seek (FILE_HEADER_LENGTH);
// writeEntry (raf, FILE_HEADER_LENGTH, data, type);
// }
// else
{
// check data format version info:
final long dataVersion = raf.readLong ();
if (dataVersion != IAppConstants.DATA_FORMAT_VERSION)
{
// read app version info for the error message:
int major = 0, minor = 0, build = 0;
boolean gotAppVersion = false;
try
{
major = raf.readInt ();
minor = raf.readInt ();
build = raf.readInt ();
gotAppVersion = true;
}
catch (Throwable ignore) {}
// TODO: error code here?
if (gotAppVersion)
{
throw new IOException ("cannot merge new data into [" + file.getAbsolutePath () + "]: created by another " + IAppConstants.APP_NAME + " version [" + makeAppVersion (major, minor, build) + "]");
}
else
{
throw new IOException ("cannot merge new data into [" + file.getAbsolutePath () + "]: created by another " + IAppConstants.APP_NAME + " version");
}
}
else
{
// [assertion: file header is valid and data format version is consistent]
raf.seek (FILE_HEADER_LENGTH);
if (length == FILE_HEADER_LENGTH)
{
// no previous data entries: append 'data'
writeEntry (log, raf, FILE_HEADER_LENGTH, data, type);
}
else
{
// [assertion: file length > FILE_HEADER_LENGTH]
// write 'data' starting with the first corrupt entry or the end of the file:
long position = FILE_HEADER_LENGTH;
long entryLength;
while (true)
{
if (trace2) log.trace2 (method, "[" + file + "]: position " + raf.getFilePointer ());
if (position >= length) break;
entryLength = raf.readLong ();
if ((entryLength <= 0) || (position + entryLength + ENTRY_HEADER_LENGTH > length))
break;
else
{
if (trace2) log.trace2 (method, "[" + file + "]: found valid entry of size " + entryLength);
position += entryLength + ENTRY_HEADER_LENGTH;
raf.seek (position);
}
}
if (trace2) log.trace2 (method, "[" + file + "]: adding entry at position " + position);
writeEntry (log, raf, position, data, type);
}
}
}
}
}
}
else
{
// 'file' does not exist:
if (trace1) log.trace1 (method, "[" + file + "]: creating a new file");
final File parent = file.getParentFile ();
if (parent != null) parent.mkdirs ();
raf = new RandomAccessFile (file, "rw");
overwrite = true;
}
if (overwrite)
{
// persist starting from 0 offset:
if ($assert.ENABLED) $assert.ASSERT (raf != null, "raf = null");
if (truncate) raf.seek (0);
writeFileHeader (raf);
if ($assert.ENABLED) $assert.ASSERT (raf.getFilePointer () == FILE_HEADER_LENGTH, "invalid header length: " + raf.getFilePointer ());
writeEntry (log, raf, FILE_HEADER_LENGTH, data, type);
}
}
finally
{
if (raf != null) try { raf.close (); } catch (Throwable ignore) {}
raf = null;
}
if (trace1)
{
end = System.currentTimeMillis ();
log.trace1 (method, "[" + file + "]: file processed in " + (end - start) + " ms");
}
}
private static void writeFileHeader (final DataOutput out)
throws IOException
{
out.writeInt (MAGIC);
out.writeLong (IAppConstants.DATA_FORMAT_VERSION);
out.writeInt (IAppConstants.APP_MAJOR_VERSION);
out.writeInt (IAppConstants.APP_MINOR_VERSION);
out.writeInt (IAppConstants.APP_BUILD_ID);
}
private static void writeEntryHeader (final DataOutput out, final byte type)
throws IOException
{
out.writeLong (UNKNOWN); // length placeholder
out.writeByte (type);
}
private static void writeEntry (final Logger log, final RandomAccessFile raf, final long marker, final IMergeable data, final byte type)
throws IOException
{
// [unfinished] entry header:
writeEntryHeader (raf, type);
// serialize 'data' starting with the current raf position:
RandomAccessFileOutputStream rafout = new RandomAccessFileOutputStream (raf, IO_BUF_SIZE); // note: no new file descriptors created here
{
// ObjectOutputStream oout = new ObjectOutputStream (rafout);
//
// oout.writeObject (data);
// oout.flush ();
// oout = null;
DataOutputStream dout = new DataOutputStream (rafout);
switch (type)
{
case TYPE_METADATA: MetaData.writeExternal ((MetaData) data, dout);
break;
default /* TYPE_COVERAGEDATA */: CoverageData.writeExternal ((CoverageData) data, dout);
break;
} // end of switch
dout.flush ();
dout = null;
// truncate:
raf.setLength (raf.getFilePointer ());
}
// transact this entry [finish the header]:
raf.seek (marker);
raf.writeLong (rafout.getCount ());
if (DO_FSYNC) raf.getFD ().sync ();
if (log.atTRACE2 ()) log.trace2 ("writeEntry", "entry [" + data.getClass ().getName () + "] length: " + rafout.getCount ());
}
private static IMergeable readEntry (final RandomAccessFile raf, final byte type, final long entryLength)
throws IOException
{
final Object data;
RandomAccessFileInputStream rafin = new RandomAccessFileInputStream (raf, IO_BUF_SIZE); // note: no new file descriptors created here
{
// ObjectInputStream oin = new ObjectInputStream (rafin);
//
// try
// {
// data = oin.readObject ();
// }
// catch (ClassNotFoundException cnfe)
// {
// // TODO: EMMA exception here
// throw new IOException ("could not read data entry: " + cnfe.toString ());
// }
DataInputStream din = new DataInputStream (rafin);
switch (type)
{
case TYPE_METADATA: data = MetaData.readExternal (din);
break;
default /* TYPE_COVERAGEDATA */: data = CoverageData.readExternal (din);
break;
} // end of switch
}
if ($assert.ENABLED) $assert.ASSERT (rafin.getCount () == entryLength, "entry length mismatch: " + rafin.getCount () + " != " + entryLength);
return (IMergeable) data;
}
/*
* This is cloned from EMMAProperties by design, to eliminate a CONSTANT_Class_info
* dependency between this and EMMAProperties classes.
*/
private static String makeAppVersion (final int major, final int minor, final int build)
{
final StringBuffer buf = new StringBuffer ();
buf.append (major);
buf.append ('.');
buf.append (minor);
buf.append ('.');
buf.append (build);
return buf.toString ();
}
private static final int NULL_ARRAY_LENGTH = -1;
private static final int MAGIC = 0x454D4D41; // "EMMA"
private static final long UNKNOWN = 0L;
private static final int FILE_HEADER_LENGTH = 4 + 8 + 3 * 4; // IMPORTANT: update on writeFileHeader() changes
private static final int ENTRY_HEADER_LENGTH = 8 + 1; // IMPORTANT: update on writeEntryHeader() changes
private static final boolean DO_FSYNC = true;
private static final int IO_BUF_SIZE = 32 * 1024;
} // end of class
// ----------------------------------------------------------------------------