1 /* Copyright (C) 2003 Vladimir Roubtsov. All rights reserved. 2 * 3 * This program and the accompanying materials are made available under 4 * the terms of the Common Public License v1.0 which accompanies this distribution, 5 * and is available at http://www.eclipse.org/legal/cpl-v10.html 6 * 7 * $Id: DataFactory.java,v 1.1.1.1.2.3 2004/07/16 23:32:29 vlad_r Exp $ 8 */ 9 package com.vladium.emma.data; 10 11 import java.io.BufferedInputStream; 12 import java.io.BufferedOutputStream; 13 import java.io.DataInput; 14 import java.io.DataInputStream; 15 import java.io.DataOutput; 16 import java.io.DataOutputStream; 17 import java.io.File; 18 import java.io.FileDescriptor; 19 import java.io.FileInputStream; 20 import java.io.FileOutputStream; 21 import java.io.IOException; 22 import java.io.ObjectInputStream; 23 import java.io.ObjectOutputStream; 24 import java.io.OutputStream; 25 import java.io.RandomAccessFile; 26 import java.net.URL; 27 import java.net.URLConnection; 28 29 import com.vladium.logging.Logger; 30 import com.vladium.util.asserts.$assert; 31 import com.vladium.emma.IAppConstants; 32 33 // ---------------------------------------------------------------------------- 34 /** 35 * @author Vlad Roubtsov, (C) 2003 36 */ 37 public 38 abstract class DataFactory 39 { 40 // public: ................................................................ 41 42 // TODO: file compaction 43 // TODO: file locking 44 45 // TODO: what's the best place for these? 46 47 public static final byte TYPE_METADATA = 0x0; // must start with 0 48 public static final byte TYPE_COVERAGEDATA = 0x1; // must be consistent with mergeload() 49 50 load(final File file)51 public static IMergeable [] load (final File file) 52 throws IOException 53 { 54 if (file == null) throw new IllegalArgumentException ("null input: file"); 55 56 return mergeload (file); 57 } 58 persist(final IMetaData data, final File file, final boolean merge)59 public static void persist (final IMetaData data, final File file, final boolean merge) 60 throws IOException 61 { 62 if (data == null) throw new IllegalArgumentException ("null input: data"); 63 if (file == null) throw new IllegalArgumentException ("null input: file"); 64 65 if (! merge && file.exists ()) 66 { 67 if (! file.delete ()) 68 throw new IOException ("could not delete file [" + file.getAbsolutePath () + "]"); 69 } 70 71 persist (data, TYPE_METADATA, file); 72 } 73 persist(final ICoverageData data, final File file, final boolean merge)74 public static void persist (final ICoverageData data, final File file, final boolean merge) 75 throws IOException 76 { 77 if (data == null) throw new IllegalArgumentException ("null input: data"); 78 if (file == null) throw new IllegalArgumentException ("null input: file"); 79 80 if (! merge && file.exists ()) 81 { 82 if (! file.delete ()) 83 throw new IOException ("could not delete file [" + file.getAbsolutePath () + "]"); 84 } 85 86 persist (data, TYPE_COVERAGEDATA, file); 87 } 88 persist(final ISessionData data, final File file, final boolean merge)89 public static void persist (final ISessionData data, final File file, final boolean merge) 90 throws IOException 91 { 92 if (data == null) throw new IllegalArgumentException ("null input: data"); 93 if (file == null) throw new IllegalArgumentException ("null input: file"); 94 95 if (! merge && file.exists ()) 96 { 97 if (! file.delete ()) 98 throw new IOException ("could not delete file [" + file.getAbsolutePath () + "]"); 99 } 100 101 persist (data.getMetaData (), TYPE_METADATA, file); 102 persist (data.getCoverageData (), TYPE_COVERAGEDATA, file); 103 } 104 105 newMetaData(final CoverageOptions options)106 public static IMetaData newMetaData (final CoverageOptions options) 107 { 108 return new MetaData (options); 109 } 110 newCoverageData()111 public static ICoverageData newCoverageData () 112 { 113 return new CoverageData (); 114 } 115 readMetaData(final URL url)116 public static IMetaData readMetaData (final URL url) 117 throws IOException, ClassNotFoundException 118 { 119 ObjectInputStream oin = null; 120 121 try 122 { 123 oin = new ObjectInputStream (new BufferedInputStream (url.openStream (), 32 * 1024)); 124 125 return (IMetaData) oin.readObject (); 126 } 127 finally 128 { 129 if (oin != null) try { oin.close (); } catch (Exception ignore) {} 130 } 131 } 132 writeMetaData(final IMetaData data, final OutputStream out)133 public static void writeMetaData (final IMetaData data, final OutputStream out) 134 throws IOException 135 { 136 ObjectOutputStream oout = new ObjectOutputStream (out); 137 oout.writeObject (data); 138 } 139 writeMetaData(final IMetaData data, final URL url)140 public static void writeMetaData (final IMetaData data, final URL url) 141 throws IOException 142 { 143 final URLConnection connection = url.openConnection (); 144 connection.setDoOutput (true); 145 146 OutputStream out = null; 147 try 148 { 149 out = connection.getOutputStream (); 150 151 writeMetaData (data, out); 152 out.flush (); 153 } 154 finally 155 { 156 if (out != null) try { out.close (); } catch (Exception ignore) {} 157 } 158 } 159 readCoverageData(final URL url)160 public static ICoverageData readCoverageData (final URL url) 161 throws IOException, ClassNotFoundException 162 { 163 ObjectInputStream oin = null; 164 165 try 166 { 167 oin = new ObjectInputStream (new BufferedInputStream (url.openStream (), 32 * 1024)); 168 169 return (ICoverageData) oin.readObject (); 170 } 171 finally 172 { 173 if (oin != null) try { oin.close (); } catch (Exception ignore) {} 174 } 175 } 176 writeCoverageData(final ICoverageData data, final OutputStream out)177 public static void writeCoverageData (final ICoverageData data, final OutputStream out) 178 throws IOException 179 { 180 // TODO: prevent concurrent modification problems here 181 182 ObjectOutputStream oout = new ObjectOutputStream (out); 183 oout.writeObject (data); 184 } 185 readIntArray(final DataInput in)186 public static int [] readIntArray (final DataInput in) 187 throws IOException 188 { 189 final int length = in.readInt (); 190 if (length == NULL_ARRAY_LENGTH) 191 return null; 192 else 193 { 194 final int [] result = new int [length]; 195 196 // read array in reverse order: 197 for (int i = length; -- i >= 0; ) 198 { 199 result [i] = in.readInt (); 200 } 201 202 return result; 203 } 204 } 205 readBooleanArray(final DataInput in)206 public static boolean [] readBooleanArray (final DataInput in) 207 throws IOException 208 { 209 final int length = in.readInt (); 210 if (length == NULL_ARRAY_LENGTH) 211 return null; 212 else 213 { 214 final boolean [] result = new boolean [length]; 215 216 // read array in reverse order: 217 for (int i = length; -- i >= 0; ) 218 { 219 result [i] = in.readBoolean (); 220 } 221 222 return result; 223 } 224 } 225 writeIntArray(final int [] array, final DataOutput out)226 public static void writeIntArray (final int [] array, final DataOutput out) 227 throws IOException 228 { 229 if (array == null) 230 out.writeInt (NULL_ARRAY_LENGTH); 231 else 232 { 233 final int length = array.length; 234 out.writeInt (length); 235 236 // write array in reverse order: 237 for (int i = length; -- i >= 0; ) 238 { 239 out.writeInt (array [i]); 240 } 241 } 242 } 243 writeBooleanArray(final boolean [] array, final DataOutput out)244 public static void writeBooleanArray (final boolean [] array, final DataOutput out) 245 throws IOException 246 { 247 if (array == null) 248 out.writeInt (NULL_ARRAY_LENGTH); 249 else 250 { 251 final int length = array.length; 252 out.writeInt (length); 253 254 // write array in reverse order: 255 for (int i = length; -- i >= 0; ) 256 { 257 out.writeBoolean (array [i]); 258 } 259 } 260 } 261 262 // protected: ............................................................. 263 264 // package: ............................................................... 265 266 // private: ............................................................... 267 268 269 private static final class UCFileInputStream extends FileInputStream 270 { close()271 public void close () 272 { 273 } 274 UCFileInputStream(final FileDescriptor fd)275 UCFileInputStream (final FileDescriptor fd) 276 { 277 super (fd); 278 279 if ($assert.ENABLED) $assert.ASSERT (fd.valid (), "UCFileInputStream.<init>: FD invalid"); 280 } 281 282 } // end of nested class 283 284 private static final class UCFileOutputStream extends FileOutputStream 285 { close()286 public void close () 287 { 288 } 289 UCFileOutputStream(final FileDescriptor fd)290 UCFileOutputStream (final FileDescriptor fd) 291 { 292 super (fd); 293 294 if ($assert.ENABLED) $assert.ASSERT (fd.valid (), "UCFileOutputStream.<init>: FD invalid"); 295 } 296 297 } // end of nested class 298 299 300 private static final class RandomAccessFileInputStream extends BufferedInputStream 301 { read()302 public final int read () throws IOException 303 { 304 final int rc = super.read (); 305 if (rc >= 0) ++ m_count; 306 307 return rc; 308 } 309 read(final byte [] b, final int off, final int len)310 public final int read (final byte [] b, final int off, final int len) 311 throws IOException 312 { 313 final int rc = super.read (b, off, len); 314 if (rc >= 0) m_count += rc; 315 316 return rc; 317 } 318 read(final byte [] b)319 public final int read (final byte [] b) throws IOException 320 { 321 final int rc = super.read (b); 322 if (rc >= 0) m_count += rc; 323 324 return rc; 325 } 326 close()327 public void close () 328 { 329 } 330 331 RandomAccessFileInputStream(final RandomAccessFile raf, final int bufSize)332 RandomAccessFileInputStream (final RandomAccessFile raf, final int bufSize) 333 throws IOException 334 { 335 super (new UCFileInputStream (raf.getFD ()), bufSize); 336 } 337 getCount()338 final long getCount () 339 { 340 return m_count; 341 } 342 343 private long m_count; 344 345 } // end of nested class 346 347 private static final class RandomAccessFileOutputStream extends BufferedOutputStream 348 { write(final byte [] b, final int off, final int len)349 public final void write (final byte [] b, final int off, final int len) throws IOException 350 { 351 super.write (b, off, len); 352 m_count += len; 353 } 354 write(final byte [] b)355 public final void write (final byte [] b) throws IOException 356 { 357 super.write (b); 358 m_count += b.length; 359 } 360 write(final int b)361 public final void write (final int b) throws IOException 362 { 363 super.write (b); 364 ++ m_count; 365 } 366 close()367 public void close () 368 { 369 } 370 371 RandomAccessFileOutputStream(final RandomAccessFile raf, final int bufSize)372 RandomAccessFileOutputStream (final RandomAccessFile raf, final int bufSize) 373 throws IOException 374 { 375 super (new UCFileOutputStream (raf.getFD ()), bufSize); 376 } 377 getCount()378 final long getCount () 379 { 380 return m_count; 381 } 382 383 private long m_count; 384 385 } // end of nested class 386 387 DataFactory()388 private DataFactory () {} // prevent subclassing 389 390 /* 391 * input checked by the caller 392 */ mergeload(final File file)393 private static IMergeable [] mergeload (final File file) 394 throws IOException 395 { 396 final Logger log = Logger.getLogger (); 397 final boolean trace1 = log.atTRACE1 (); 398 final boolean trace2 = log.atTRACE2 (); 399 final String method = "mergeload"; 400 401 long start = 0, end; 402 403 if (trace1) start = System.currentTimeMillis (); 404 405 final IMergeable [] result = new IMergeable [2]; 406 407 if (! file.exists ()) 408 { 409 throw new IOException ("input file does not exist: [" + file.getAbsolutePath () + "]"); 410 } 411 else 412 { 413 RandomAccessFile raf = null; 414 try 415 { 416 raf = new RandomAccessFile (file, "r"); 417 418 // 'file' is a valid existing file, but it could still be of 0 length or otherwise corrupt: 419 final long length = raf.length (); 420 if (trace1) log.trace1 (method, "[" + file + "]: file length = " + length); 421 422 if (length < FILE_HEADER_LENGTH) 423 { 424 throw new IOException ("file [" + file.getAbsolutePath () + "] is corrupt or was not created by " + IAppConstants.APP_NAME); 425 } 426 else 427 { 428 // TODO: data version checks parallel to persist() 429 430 if (length > FILE_HEADER_LENGTH) // return {null, null} in case of equality 431 { 432 raf.seek (FILE_HEADER_LENGTH); 433 434 // [assertion: file length > FILE_HEADER_LENGTH] 435 436 // read entries until the first corrupt entry or the end of the file: 437 438 long position = FILE_HEADER_LENGTH; 439 long entryLength; 440 441 long entrystart = 0; 442 443 while (true) 444 { 445 if (trace2) log.trace2 (method, "[" + file + "]: position " + raf.getFilePointer ()); 446 if (position >= length) break; 447 448 entryLength = raf.readLong (); 449 450 if ((entryLength <= 0) || (position + entryLength + ENTRY_HEADER_LENGTH > length)) 451 break; 452 else 453 { 454 final byte type = raf.readByte (); 455 if ((type < 0) || (type >= result.length)) 456 break; 457 458 if (trace2) log.trace2 (method, "[" + file + "]: found valid entry of size " + entryLength + " and type " + type); 459 { 460 if (trace2) entrystart = System.currentTimeMillis (); 461 final IMergeable data = readEntry (raf, type, entryLength); 462 if (trace2) log.trace2 (method, "entry read in " + (System.currentTimeMillis () - entrystart) + " ms"); 463 464 final IMergeable current = result [type]; 465 466 if (current == null) 467 result [type] = data; 468 else 469 result [type] = current.merge (data); // note: later entries overrides earlier entries 470 } 471 472 position += entryLength + ENTRY_HEADER_LENGTH; 473 474 if ($assert.ENABLED) $assert.ASSERT (raf.getFD ().valid (), "FD invalid"); 475 raf.seek (position); 476 } 477 } 478 } 479 } 480 } 481 finally 482 { 483 if (raf != null) try { raf.close (); } catch (Throwable ignore) {} 484 raf = null; 485 } 486 } 487 488 if (trace1) 489 { 490 end = System.currentTimeMillis (); 491 492 log.trace1 (method, "[" + file + "]: file processed in " + (end - start) + " ms"); 493 } 494 495 return result; 496 } 497 498 499 /* 500 * input checked by the caller 501 */ persist(final IMergeable data, final byte type, final File file)502 private static void persist (final IMergeable data, final byte type, final File file) 503 throws IOException 504 { 505 final Logger log = Logger.getLogger (); 506 final boolean trace1 = log.atTRACE1 (); 507 final boolean trace2 = log.atTRACE2 (); 508 final String method = "persist"; 509 510 long start = 0, end; 511 512 if (trace1) start = System.currentTimeMillis (); 513 514 // TODO: 1.4 adds some interesting RAF open mode options as well 515 // TODO: will this benefit from extra buffering? 516 517 // TODO: data version checks 518 519 RandomAccessFile raf = null; 520 try 521 { 522 boolean overwrite = false; 523 boolean truncate = false; 524 525 if (file.exists ()) 526 { 527 // 'file' exists: 528 529 if (! file.isFile ()) throw new IOException ("can persist in normal files only: " + file.getAbsolutePath ()); 530 531 raf = new RandomAccessFile (file, "rw"); 532 533 // 'file' is a valid existing file, but it could still be of 0 length or otherwise corrupt: 534 final long length = raf.length (); 535 if (trace1) log.trace1 (method, "[" + file + "]: existing file length = " + length); 536 537 538 if (length < 4) 539 { 540 overwrite = true; 541 truncate = (length > 0); 542 } 543 else 544 { 545 // [assertion: file length >= 4] 546 547 // check header info before reading further: 548 final int magic = raf.readInt (); 549 if (magic != MAGIC) 550 throw new IOException ("cannot overwrite [" + file.getAbsolutePath () + "]: not created by " + IAppConstants.APP_NAME); 551 552 if (length < FILE_HEADER_LENGTH) 553 { 554 // it's our file, but the header is corrupt: overwrite 555 overwrite = true; 556 truncate = true; 557 } 558 else 559 { 560 // [assertion: file length >= FILE_HEADER_LENGTH] 561 562 // if (! append) 563 // { 564 // // overwrite any existing data: 565 // 566 // raf.seek (FILE_HEADER_LENGTH); 567 // writeEntry (raf, FILE_HEADER_LENGTH, data, type); 568 // } 569 // else 570 { 571 // check data format version info: 572 final long dataVersion = raf.readLong (); 573 574 if (dataVersion != IAppConstants.DATA_FORMAT_VERSION) 575 { 576 // read app version info for the error message: 577 578 int major = 0, minor = 0, build = 0; 579 boolean gotAppVersion = false; 580 try 581 { 582 major = raf.readInt (); 583 minor = raf.readInt (); 584 build = raf.readInt (); 585 586 gotAppVersion = true; 587 } 588 catch (Throwable ignore) {} 589 590 // TODO: error code here? 591 if (gotAppVersion) 592 { 593 throw new IOException ("cannot merge new data into [" + file.getAbsolutePath () + "]: created by another " + IAppConstants.APP_NAME + " version [" + makeAppVersion (major, minor, build) + "]"); 594 } 595 else 596 { 597 throw new IOException ("cannot merge new data into [" + file.getAbsolutePath () + "]: created by another " + IAppConstants.APP_NAME + " version"); 598 } 599 } 600 else 601 { 602 // [assertion: file header is valid and data format version is consistent] 603 604 raf.seek (FILE_HEADER_LENGTH); 605 606 if (length == FILE_HEADER_LENGTH) 607 { 608 // no previous data entries: append 'data' 609 610 writeEntry (log, raf, FILE_HEADER_LENGTH, data, type); 611 } 612 else 613 { 614 // [assertion: file length > FILE_HEADER_LENGTH] 615 616 // write 'data' starting with the first corrupt entry or the end of the file: 617 618 long position = FILE_HEADER_LENGTH; 619 long entryLength; 620 621 while (true) 622 { 623 if (trace2) log.trace2 (method, "[" + file + "]: position " + raf.getFilePointer ()); 624 if (position >= length) break; 625 626 entryLength = raf.readLong (); 627 628 if ((entryLength <= 0) || (position + entryLength + ENTRY_HEADER_LENGTH > length)) 629 break; 630 else 631 { 632 if (trace2) log.trace2 (method, "[" + file + "]: found valid entry of size " + entryLength); 633 634 position += entryLength + ENTRY_HEADER_LENGTH; 635 raf.seek (position); 636 } 637 } 638 639 if (trace2) log.trace2 (method, "[" + file + "]: adding entry at position " + position); 640 writeEntry (log, raf, position, data, type); 641 } 642 } 643 } 644 } 645 } 646 } 647 else 648 { 649 // 'file' does not exist: 650 651 if (trace1) log.trace1 (method, "[" + file + "]: creating a new file"); 652 653 final File parent = file.getParentFile (); 654 if (parent != null) parent.mkdirs (); 655 656 raf = new RandomAccessFile (file, "rw"); 657 658 overwrite = true; 659 } 660 661 662 if (overwrite) 663 { 664 // persist starting from 0 offset: 665 666 if ($assert.ENABLED) $assert.ASSERT (raf != null, "raf = null"); 667 668 if (truncate) raf.seek (0); 669 writeFileHeader (raf); 670 if ($assert.ENABLED) $assert.ASSERT (raf.getFilePointer () == FILE_HEADER_LENGTH, "invalid header length: " + raf.getFilePointer ()); 671 672 writeEntry (log, raf, FILE_HEADER_LENGTH, data, type); 673 } 674 } 675 finally 676 { 677 if (raf != null) try { raf.close (); } catch (Throwable ignore) {} 678 raf = null; 679 } 680 681 if (trace1) 682 { 683 end = System.currentTimeMillis (); 684 685 log.trace1 (method, "[" + file + "]: file processed in " + (end - start) + " ms"); 686 } 687 } 688 writeFileHeader(final DataOutput out)689 private static void writeFileHeader (final DataOutput out) 690 throws IOException 691 { 692 out.writeInt (MAGIC); 693 694 out.writeLong (IAppConstants.DATA_FORMAT_VERSION); 695 696 out.writeInt (IAppConstants.APP_MAJOR_VERSION); 697 out.writeInt (IAppConstants.APP_MINOR_VERSION); 698 out.writeInt (IAppConstants.APP_BUILD_ID); 699 } 700 writeEntryHeader(final DataOutput out, final byte type)701 private static void writeEntryHeader (final DataOutput out, final byte type) 702 throws IOException 703 { 704 out.writeLong (UNKNOWN); // length placeholder 705 out.writeByte (type); 706 } 707 writeEntry(final Logger log, final RandomAccessFile raf, final long marker, final IMergeable data, final byte type)708 private static void writeEntry (final Logger log, final RandomAccessFile raf, final long marker, final IMergeable data, final byte type) 709 throws IOException 710 { 711 // [unfinished] entry header: 712 writeEntryHeader (raf, type); 713 714 // serialize 'data' starting with the current raf position: 715 RandomAccessFileOutputStream rafout = new RandomAccessFileOutputStream (raf, IO_BUF_SIZE); // note: no new file descriptors created here 716 { 717 // ObjectOutputStream oout = new ObjectOutputStream (rafout); 718 // 719 // oout.writeObject (data); 720 // oout.flush (); 721 // oout = null; 722 723 DataOutputStream dout = new DataOutputStream (rafout); 724 switch (type) 725 { 726 case TYPE_METADATA: MetaData.writeExternal ((MetaData) data, dout); 727 break; 728 729 default /* TYPE_COVERAGEDATA */: CoverageData.writeExternal ((CoverageData) data, dout); 730 break; 731 732 } // end of switch 733 dout.flush (); 734 dout = null; 735 736 // truncate: 737 raf.setLength (raf.getFilePointer ()); 738 } 739 740 // transact this entry [finish the header]: 741 raf.seek (marker); 742 raf.writeLong (rafout.getCount ()); 743 if (DO_FSYNC) raf.getFD ().sync (); 744 745 if (log.atTRACE2 ()) log.trace2 ("writeEntry", "entry [" + data.getClass ().getName () + "] length: " + rafout.getCount ()); 746 } 747 readEntry(final RandomAccessFile raf, final byte type, final long entryLength)748 private static IMergeable readEntry (final RandomAccessFile raf, final byte type, final long entryLength) 749 throws IOException 750 { 751 final Object data; 752 753 RandomAccessFileInputStream rafin = new RandomAccessFileInputStream (raf, IO_BUF_SIZE); // note: no new file descriptors created here 754 { 755 // ObjectInputStream oin = new ObjectInputStream (rafin); 756 // 757 // try 758 // { 759 // data = oin.readObject (); 760 // } 761 // catch (ClassNotFoundException cnfe) 762 // { 763 // // TODO: EMMA exception here 764 // throw new IOException ("could not read data entry: " + cnfe.toString ()); 765 // } 766 767 DataInputStream din = new DataInputStream (rafin); 768 switch (type) 769 { 770 case TYPE_METADATA: data = MetaData.readExternal (din); 771 break; 772 773 default /* TYPE_COVERAGEDATA */: data = CoverageData.readExternal (din); 774 break; 775 776 } // end of switch 777 } 778 779 if ($assert.ENABLED) $assert.ASSERT (rafin.getCount () == entryLength, "entry length mismatch: " + rafin.getCount () + " != " + entryLength); 780 781 return (IMergeable) data; 782 } 783 784 785 /* 786 * This is cloned from EMMAProperties by design, to eliminate a CONSTANT_Class_info 787 * dependency between this and EMMAProperties classes. 788 */ makeAppVersion(final int major, final int minor, final int build)789 private static String makeAppVersion (final int major, final int minor, final int build) 790 { 791 final StringBuffer buf = new StringBuffer (); 792 793 buf.append (major); 794 buf.append ('.'); 795 buf.append (minor); 796 buf.append ('.'); 797 buf.append (build); 798 799 return buf.toString (); 800 } 801 802 803 private static final int NULL_ARRAY_LENGTH = -1; 804 805 private static final int MAGIC = 0x454D4D41; // "EMMA" 806 private static final long UNKNOWN = 0L; 807 private static final int FILE_HEADER_LENGTH = 4 + 8 + 3 * 4; // IMPORTANT: update on writeFileHeader() changes 808 private static final int ENTRY_HEADER_LENGTH = 8 + 1; // IMPORTANT: update on writeEntryHeader() changes 809 private static final boolean DO_FSYNC = true; 810 private static final int IO_BUF_SIZE = 32 * 1024; 811 812 } // end of class 813 // ---------------------------------------------------------------------------- 814