• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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