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