/* 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 // ----------------------------------------------------------------------------