DataFactory.java revision a921fd048da6858dc24d4370e3bba15fc9cc69ca
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 */ 9package com.vladium.emma.data; 10 11import java.io.BufferedInputStream; 12import java.io.BufferedOutputStream; 13import java.io.DataInput; 14import java.io.DataInputStream; 15import java.io.DataOutput; 16import java.io.DataOutputStream; 17import java.io.File; 18import java.io.FileDescriptor; 19import java.io.FileInputStream; 20import java.io.FileOutputStream; 21import java.io.IOException; 22import java.io.ObjectInputStream; 23import java.io.ObjectOutputStream; 24import java.io.OutputStream; 25import java.io.RandomAccessFile; 26import java.net.URL; 27import java.net.URLConnection; 28 29import com.vladium.logging.Logger; 30import com.vladium.util.asserts.$assert; 31import com.vladium.emma.IAppConstants; 32 33// ---------------------------------------------------------------------------- 34/** 35 * @author Vlad Roubtsov, (C) 2003 36 */ 37public 38abstract 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 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 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 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 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 106 public static IMetaData newMetaData (final CoverageOptions options) 107 { 108 return new MetaData (options); 109 } 110 111 public static ICoverageData newCoverageData () 112 { 113 return new CoverageData (); 114 } 115 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 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 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 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 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 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 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 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 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 { 271 public void close () 272 { 273 } 274 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 { 286 public void close () 287 { 288 } 289 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 { 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 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 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 327 public void close () 328 { 329 } 330 331 332 RandomAccessFileInputStream (final RandomAccessFile raf, final int bufSize) 333 throws IOException 334 { 335 super (new UCFileInputStream (raf.getFD ()), bufSize); 336 } 337 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 { 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 355 public final void write (final byte [] b) throws IOException 356 { 357 super.write (b); 358 m_count += b.length; 359 } 360 361 public final void write (final int b) throws IOException 362 { 363 super.write (b); 364 ++ m_count; 365 } 366 367 public void close () 368 { 369 } 370 371 372 RandomAccessFileOutputStream (final RandomAccessFile raf, final int bufSize) 373 throws IOException 374 { 375 super (new UCFileOutputStream (raf.getFD ()), bufSize); 376 } 377 378 final long getCount () 379 { 380 return m_count; 381 } 382 383 private long m_count; 384 385 } // end of nested class 386 387 388 private DataFactory () {} // prevent subclassing 389 390 /* 391 * input checked by the caller 392 */ 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 */ 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 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 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 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 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 */ 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