001    /*
002    Copyright (c) 1996-2011, Damon Hart-Davis
003    All rights reserved.
004    
005    Redistribution and use in source and binary forms, with or without
006    modification, are permitted provided that the following conditions are
007    met:
008    
009      * Redistributions of source code must retain the above copyright
010        notice, this list of conditions and the following disclaimer.
011    
012      * Redistributions in binary form must reproduce the above copyright
013        notice, this list of conditions and the following disclaimer in the
014        documentation and/or other materials provided with the
015        distribution.
016    
017    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
018    IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
019    TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
020    PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
021    OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
022    SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
023    LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
024    DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
025    THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
026    (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
027    OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
028    */
029    package org.hd.d.pg2k.svrCore.MIME;
030    
031    import java.io.DataInputStream;
032    import java.io.File;
033    import java.io.FileInputStream;
034    import java.io.IOException;
035    import java.io.InputStream;
036    import java.io.InvalidObjectException;
037    import java.util.Arrays;
038    import java.util.Collections;
039    import java.util.HashSet;
040    import java.util.Hashtable;
041    import java.util.Set;
042    
043    import org.hd.d.pg2k.svrCore.FileTools;
044    
045    /**Routines to establish/check exhibit MIME types.
046     * This is hard-wired into the Gallery for now; we don't allow dynamic
047     * loading of new types and handlers...
048     * <p>
049     * This is a rewrite and extension of the old (pre-PG2K) PG Attributes class.
050     */
051    public final class ExhibitMIME
052        {
053        /**Package for default media handlers. */
054        public static final String DEFAULT_HANDLER_PACKAGE =
055            "org.hd.d.pg2k.svrCore.mediahandler";
056    
057        /**JavaBean outlining file details of one Gallery exhibit item type.
058         * This object is immutable.
059         * <p>
060         * Not designed to be persisted, so not Serializable.
061         * <p>
062         * Note that this has no public no-arg constructor because
063         * these objects are all created statically and we create
064         * all the instances that should be created.
065         * <p>
066         * Note that this class is based on the assumption that we
067         * recognise exactly one filename suffix for each type of
068         * file archived in the Gallery, and when generating something
069         * of that type (usually) use one, different, suffix.  Both
070         * suffixes are chose to be generally acceptable to most tools
071         * such as browsers.
072         * <p>
073         * These suffixes presented are intern()ed so that the number
074         * of copies of them kicking around can be reduced.  Don't go
075         * casually making up lots of random file-types...
076         */
077        public static final class ExhibitTypeParameters implements Comparable<ExhibitTypeParameters>
078            {
079            /**Make a new MIME type descriptor.
080             * Private constructor and object not (de)serialisable
081             * so as to ensure that only we can make instances here at run-time.
082             *
083             * @param _handlerClassName  the fully-qualified class name
084             *     of the handler for this MIME type; if null a default
085             *     of DEFAULT_HANDLER_PACKAGE package with the class name
086             *     the same as the primary (all-lower-case) suffix
087             *     is used instead
088             */
089            private ExhibitTypeParameters(final int _type,
090                                          final String _suffixForInputFile,
091                                          final String _mimeType,
092                                          final String _handlerClassName,
093                                          final String _magic,
094                                          final String _description)
095                {
096                this(_type, _suffixForInputFile, null, _mimeType, _handlerClassName, _magic, null, _description);
097                }
098    
099            /**Make a new MIME type descriptor.
100             * Private constructor and object not (de)serialisable
101             * so as to ensure that only we can make instances here
102             * and at run-time.
103             *
104             * @param _allSuffixes  null or a set of (dotless) extensions
105             *     (possibly including the primary one)
106             *     by which we may recognise input files of this type
107             * @param _allMagics  null or a set of magic numbers/strings
108             *     (possibly including the primary one if not "")
109             *     by which we may recognise input files of this type
110             *
111             * @param _handlerClassName  the fully-qualified class name
112             *     of the handler for this MIME type; if null a default
113             *     of DEFAULT_HANDLER_PACKAGE package with the class name
114             *     the same as the primary (all-lower-case) suffix
115             *     is used instead
116             */
117            private ExhibitTypeParameters(final int _type,
118                                          final String _suffixForInputFile,
119                                          final String _allSuffixes[],
120                                          final String _mimeType,
121                                          String _handlerClassName,
122                                          final String _magic,
123                                          final String _allMagics[],
124                                          final String _description)
125                {
126                if((_type < ET__min) || (_type > ET__max) ||
127                   (_suffixForInputFile == null) ||
128                   (_suffixForInputFile.length() < 1) ||
129                   (_suffixForInputFile.startsWith(".")) ||
130                   (_mimeType == null) ||
131                   (_mimeType.length() < 3) || // Must be at least "a/b"...
132                   (_mimeType.indexOf('/') < 1) || // Must be at least "a/b"...
133                   (!_mimeType.equals(_mimeType.toLowerCase())) || // Must be lower-case.
134                   (_magic == null) ||
135                   ("".equals(_handlerClassName)) || // Must not be empty string.
136                   (_description == null))
137                    {
138                    throw new IllegalArgumentException();
139                    }
140                type = _type;
141                suffixForInputFile = _suffixForInputFile.intern();
142                dotSuffixForInputFile = ("." + _suffixForInputFile).intern();
143                mimeType = _mimeType;
144                magic = _magic;
145                description = _description;
146    
147                // Set up the suffixes list...
148                if(_allSuffixes == null)
149                    {
150                    allSuffixesForFile = Collections.singleton(suffixForInputFile);
151                    }
152                else
153                    {
154                    final Set<String> s = new HashSet<String>(2 + _allSuffixes.length*2);
155                    s.add(suffixForInputFile); // Ensure that the primary value is present.
156                    for(int i =  _allSuffixes.length; --i >= 0; )
157                        {
158                        final String t = _allSuffixes[i];
159                        if((t == null) || (t.length() < 1) || (t.startsWith(".")))
160                            { throw new IllegalArgumentException(); }
161                        s.add(t);
162                        }
163                    allSuffixesForFile = Collections.unmodifiableSet(s);
164                    }
165    
166                // Set up the magics list.
167                if(_allMagics == null)
168                    {
169                    if(magic.length() == 0)
170                        { allMagics = Collections.emptySet(); }
171                    else
172                        { allMagics = Collections.singleton(magic); }
173                    }
174                else
175                    {
176                    final Set<String> s = new HashSet<String>(2 + _allMagics.length*2);
177                    s.add(magic); // Ensure that the primary value is present.
178                    for(int i =  _allMagics.length; --i >= 0; )
179                        {
180                        final String t = _allMagics[i];
181                        if((t == null) || (t.length() < 1))
182                            { throw new IllegalArgumentException(); }
183                        s.add(t);
184                        }
185                    allMagics = Collections.unmodifiableSet(s);
186                    }
187    
188                // Iff no explicit handler has been supplied,
189                // and the default one appears to at least exist,
190                // use the default one.
191                if(_handlerClassName == null)
192                    {
193                    // Compute the name for the default handler class for this
194                    // MIME type.
195                    final String defaultHandlerClassName =
196                        DEFAULT_HANDLER_PACKAGE + dotSuffixForInputFile.toLowerCase();
197                    try {
198                        // Just see if the default handler exists;
199                        // report more subtle errors later.
200                        Class.forName(defaultHandlerClassName);
201                        // OK, seems to exist, so use it.
202                        _handlerClassName = defaultHandlerClassName;
203                        }
204                    // Ignore errors, but don't try to use the default handler.
205                    catch(final Exception e) { }
206                    }
207    
208                // Create instance of handler class.
209                Handler h = null;
210                try {
211                    if(_handlerClassName != null)
212                        {
213                        h = (Handler) ((Class.forName(_handlerClassName)).newInstance());
214                        }
215                    }
216                // Note any errors we encounter.
217                catch(final Exception e)
218                    {
219                    System.err.println("ExhibitMIME: ERROR: could not create handler for MIME type "+mimeType+" with class ``"+_handlerClassName+"''.");
220                    e.printStackTrace();
221                    }
222                // Keep the name and handler null if not successfully instantiated.
223                handler = h;
224                handlerClassName = (handler != null) ? _handlerClassName : null;
225                }
226    
227    
228            /**Make meaningful human-readable diagnostic String representation.
229             * This is not intended to be machine-parsable,
230             * but does not contain whitespace if possible so it might be.
231             *
232             * @return  human-readable indication of exhibit type.
233             */
234            @Override
235            public String toString()
236                {
237                final StringBuilder result = new StringBuilder(48);
238                result.append("ExhibitTypeParameters:");
239                result.append(suffixForInputFile).append('[');
240                    result.append(type).append("]:");
241                result.append(mimeType);
242                return(result.toString());
243                }
244    
245    
246            /**ET_XXX type of file. */
247            public final int type;
248    
249            /**Suffix of file if this item is an exhibit in the Gallery; lower-case, never null.
250             * This means it is an input item, and should (for example)
251             * be read-only and never deleted.
252             * <p>
253             * This does not include the dot that comes before the extension.
254             * <p>
255             * This value is never null or zero-length, and is
256             * intern()ed.
257             */
258            public final String suffixForInputFile;
259    
260            /**Suffix of input file including leading dot; lower-case, never null.
261             * This is suffixForInputFile with a leading dot, and
262             * is intern()ed, so is never null and is not less than length 2.
263             */
264            public final String dotSuffixForInputFile;
265    
266            /**All (dotless, lower-case) suffixes for this type, including the primary suffix; never null nor empty.
267             * We can recognise all these suffixes on input files
268             * as potentially indicating this MIME type.
269             * <p>
270             * No entries are null or "" or start with (or contain) a dot.
271             * <p>
272             * This is immutable.
273             */
274            public final Set<String> allSuffixesForFile;
275    
276            /**This is the primary `magic number' at the start of the file; never null though may be "".
277             * This is really a byte string, in order, of the leading
278             * bytes we must see at the start of a file to be happy
279             * it is really the type the file suffix claims it is.
280             * If zero-length, this indicates that there is no such
281             * initial portion we can check.
282             * <p>
283             * This enables a simple form of file-type checking
284             * like the UNIX ``file'' utility.
285             * <p>
286             * This string may be zero-length, but it is never
287             * null.
288             */
289            public final String magic;
290    
291            /**All legitimate `magic number' values for this type; never null.
292             * Contains the primary magic value unless it is "".
293             * <p>
294             * No entries are null or "".
295             * <p>
296             * Is empty if no magic number.
297             * <p>
298             * This is immutable.
299             */
300            public final Set<String> allMagics;
301    
302            /**The canonical MIME type for this exhibit format; never null nor "".
303             * Of the form majorType/minorType eg "audio/basic" or "text/html",
304             * with all text lower-case.
305             */
306            public final String mimeType;
307    
308            /**Fully-qualified name of handler class (implementing HandlerBase), or null if none.
309             */
310            public final String handlerClassName;
311    
312            /**Instance of handler class, or null if none.
313             */
314            public final Handler handler;
315    
316            /**Textual (English) description of the file type.
317             * This describes in human-readable terms what we
318             * expect the image to be (it may be that the file
319             * extension and magic number would allow a wider range
320             * than we describe here).
321             * <p>
322             * This is intended to be terse enough to go on a Web
323             * page with an image file if need be.  It may be zero length,
324             * though must not be null.
325             */
326            public final String description;
327    
328    
329            /**Tested for equality and sorted on type. */
330            @Override
331            public int hashCode() { return(type); }
332    
333            /**Equal if type is equal. */
334            @Override
335            public boolean equals(final Object o)
336                {
337                if(!(o instanceof ExhibitTypeParameters)) { return(false); }
338                return(type == ((ExhibitTypeParameters) o).type);
339                }
340    
341            /**Sort by type. */
342            public int compareTo(final ExhibitTypeParameters o)
343                { return(type - o.type); }
344    
345    
346            /**Returns true if we may be able to create a thumbnail/sample with the same MIME type as this.
347             * We might fail in practice, but if this returns false we know that
348             * we definitely <em>cannot</em> make a thumbnail/sample for this
349             * exhibit type in the current execution context, eg without some
350             * loadable drivers.
351             */
352            public boolean canPossiblyCreateThumbnailOfSameMIMEType()
353                {
354                // Won't be able to do this if we don't have a handler.
355                if(handler == null) { return(false); }
356    
357                // Won't be able to do this if the handler says it can't.
358                if(!handler.canMakeThumbnails()) { return(false); }
359    
360                return(true); // OK, maybe we can!
361                }
362            }
363    
364        // All file types are strictly positive.
365        // Existing values must not be changed as they may be persisted
366        // to long-lasting media.
367        /**Type of GIF file. */
368        public static final int ET_GIF  = 1;
369        /**Type of JPEG file. */
370        public static final int ET_JPEG = 2;
371        /**Type of Sun audio (usually 64kbps 8-bit u-law G.711) file. */
372        public static final int ET_AU   = 3;
373        /**Type of MPEG-1 Layer-II audio file. */
374        public static final int ET_MP2  = 4;
375        /**Type of MPEG-1 or MPEG-2 or MPEG-2.5 Layer-III audio file (8kbps and up). */
376        public static final int ET_MP3  = 5;
377        /**Type of MPEG-4 audio file (2kbps and up). */
378        public static final int ET_MP4  = 6;
379        /**Type of General MIDI audio file. */
380        public static final int ET_MIDI = 7;
381        /**Type of WAV audio file. */
382        public static final int ET_WAV = 8;
383        /**Type of HTML text fragment file. */
384        public static final int ET_HTMLFRAG = 9;
385        /**Type of MPEG video file. */
386        public static final int ET_MPEG = 10;
387        /**Type of TIFF image file. */
388        public static final int ET_TIFF = 11;
389        /**Type of BMP image file. */
390        public static final int ET_BMP = 12;
391        /**Type of PowerPoint presentation file. */
392        public static final int ET_PPT = 13;
393        /**Type of RealMedia file. */
394        public static final int ET_RM = 14;
395        /**Type of RealMedia file. */
396        public static final int ET_AVI = 15;
397        /**Type of PNG (GIF-replacement) file. */
398        public static final int ET_PNG = 16;
399        /**Type of MNG (GIF-replacement) file. */
400        public static final int ET_MNG = 17;
401        /**Type of JPEG-2000 image file. */
402        public static final int ET_JP2 = 18;
403        /**Type of Flash file. */
404        public static final int ET_SWF = 19;
405        /**Type of PDF file. */
406        public static final int ET_PDF = 20;
407        /**Type of 3GPP file. */
408        public static final int ET_3GP = 21;
409        /**Type of Windows Movie file. */
410        public static final int ET_WMV = 22;
411        /**Type of Windows Rich Text Format file. */
412        public static final int ET_RTF = 23;
413        /**Type of Gallery 'trail' marked-up text file. */
414        public static final int ET_TRML = 24;
415        /**Type of ZIP archive file. */
416        public static final int ET_ZIP = 25;
417        /**Type of GZIPped tar archive file. */
418        public static final int ET_TGZ = 26;
419        /**Type of BZIP2ed tar archive file. */
420        public static final int ET_TBZ2 = 27;
421        /**Type of QuickTime movie file. */
422        public static final int ET_MOV = 28;
423    
424        /**Minimum valid ET_XXX value (these values may be sparse).
425         * Guaranteed strictly positive.
426         */
427        public static final int ET__min = ET_GIF;
428    
429        /**Maximum valid ET_XXX value (these values may be sparse).
430         * Guaranteed to be no less than ET__min.
431         */
432        public static final int ET__max = ET_MOV;
433    
434        /**File types accepted by (and generated by) the Gallery.
435         * Not to be handed outside the gallery in this raw state.
436         * Not necessarily in numeric order at construction here,
437         * but sorted as part of the static initialiser.
438         */
439        private static final ExhibitTypeParameters inputFileTypes[] =
440            {
441            new ExhibitTypeParameters(ET_GIF,      "gif",  "image/gif",   null, "GIF8", "GIF image"),
442            // JPEG/JFIF would be ff d8 ff e0 XX XX 'J' 'F' 'I' 'F',
443            // but Mavica images seem to omit this JFIF header.
444            new ExhibitTypeParameters(ET_JPEG,     "jpg",  new String[]{"jpeg"}, "image/jpeg",  null, "\u00ff\u00d8\u00ff", null, "JPEG image"),
445            new ExhibitTypeParameters(ET_AU,       "au",   "audio/basic", null, ".snd", "Sun audio"),
446            // MP2 magic sequence may not be long enough to be unique.
447            // See opening od -tx1 dump from an .mp2 file...
448            // 0000000 00 00 01 ba 21 00 01 00 01 80 0e 3b 00 00 01 bb
449            new ExhibitTypeParameters(ET_MP2,      "mp2",  "audio/mpeg",  null, "\u0000\u0000\u0001\u00ba", "MP2 [MPEG-1 Layer-II] audio"),
450            // MP3 magic sequence may not be long enough to be unique.
451            // See opening od -tx1 dump from three .mp3 files...
452            // 0000000 ff f3 74 54 00 00 0c 01 2d 90 00 00 00 00 a8 02
453            // 0000000 ff e3 18 c4 00 0a 28 69 bb 21 46 00 01 00 20 78
454            // 0000000 ff fb b0 60 00 00 03 a7 5b 4c b3 09 13 f2 04 a0
455            // See http://www.mp3-tech.org/programmer/frame_header.html
456            // Header is: 11111111 111BBCCD EEEEFFGH IIJJKLMM
457            // Def BB: 00 => MPEG Version 2.5, 01 => reserved, 10 => MPEG Version 2 (ISO/IEC 13818-3), 11 => MPEG Version 1 (ISO/IEC 11172-3).
458            // So header of ff fx guarantees MPEG 1 or MPEG 2 (ie with 12-leading sync bits).
459            // Def CC: 00 => reserved, 01 => Layer III, 10 => Layer II, 11 => Layer I.
460            // Def D: 0 => protected by CRC (16 bit CRC follows header), 1 => not protected.
461            new ExhibitTypeParameters(ET_MP3,      "mp3",  "audio/mpeg",  null, "\u00ff", "MP3 [MPEG Layer-III] audio"),
462            //new ExhibitTypeParameters(ET_MP4,      "mp4", "MP4", "", "MPEG-4 audio"),
463            // MIDI magic number guessed by looking at a few files...
464            new ExhibitTypeParameters(ET_MIDI,     "mid",  "audio/midi",  null, "MThd", "General MIDI"),
465            // WAV magic sequence may not be long enough to be unique.
466            // See opening od -cv dump from one .wav file...
467            // 0000000   R   I   F   F 204 033  \0  \0   W   A   V   E   f   m   t
468            new ExhibitTypeParameters(ET_WAV,      "wav",  "audio/x-wav", null, "RIFF", "Wave audio"),
469            new ExhibitTypeParameters(ET_HTMLFRAG, "htxt", "text/html",   null, "<", "HTML marked-up text fragment"),
470            // Deduced magic from dump (od -tx1).
471            // 0000000 00 00 01 ba 21 00 01 00 01 80 03 91 00 00 01 bb
472            new ExhibitTypeParameters(ET_MPEG,     "mpg",  "video/mpeg",  null, "\u0000\u0000\u0001\u00ba", "MPEG video"),
473            // Deduced magic from dump (od -tx1) and RFC3302...
474            // 49 49 2a 00 (II: little endian)
475            // 4d 4d 00 2a (MM: big endian)
476            new ExhibitTypeParameters(ET_TIFF,     "tif",  new String[]{"tiff"}, "image/tiff",  null, "II*\u0000", new String[]{"MM\u0000*"}, "TIFF image"),
477            // Deduced magic from dump (od -tx1).
478            // 0000000 42 4d 76 b6 0f 00 00 00 00 00 36 00 00 00 28 00
479            new ExhibitTypeParameters(ET_BMP,      "bmp",  "image/bmp",   null, "BM", "BMP image"),
480            // Deduced magic from dump (od -tx1).
481            // 0000000 d0 cf 11 e0 a1 b1 1a e1 00 00 00 00 00 00 00 00
482            new ExhibitTypeParameters(ET_PPT,      "ppt",  "application/powerpoint",  null, "\u00d0\u00cf\u0011\u00e0\u00a1\u00b1\u001a\u00e1\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "PowerPoint presentation"),
483            // 0000000 2e 52 4d 46 00 00 00 12 00 01 00 00 00 00 00 00
484            new ExhibitTypeParameters(ET_RM,       "rm",   "application/vnd.rn-realmedia",  null, ".RMF", "RealMedia audio/video"),
485            // Magic number as suggest by http://www.jmcgowan.com/avi.html: "RIFF" <4-byte-length> "AVI ".
486            // See also http://www.fourcc.org/.
487            new ExhibitTypeParameters(ET_AVI,      "avi",  "video/avi",  null, "RIFF", "AVI audio/video"),
488            // Magic number from formal tech documentation.
489            // See http://www.libpng.org/.
490            new ExhibitTypeParameters(ET_PNG,      "png",  "image/png",  null, "\u0089PNG\r\n\u001a\n", "PNG image"),
491            // Magic number from formal tech documentation.
492            // See http://www.libpng.org/.
493            new ExhibitTypeParameters(ET_MNG,      "mng",  "video/x-mng",  null, "\u008aMNG\r\n\u001a\n", "MNG animation/video"),
494            // JPEG-2000; 12-byte magic number from spec; also see RFC3745 and jpeg.org: 0000 000C 6A50 2020 0D0A 870A.
495            new ExhibitTypeParameters(ET_JP2,      "jp2",  "image/jp2",  null, "\u0000\u0000\u0000\u000cjP  \r\n\u0087\n", "JPEG 2000 image"),
496            // Macromedia Flash: magic number as per: http://www.garykessler.net/library/file_sigs.html
497            new ExhibitTypeParameters(ET_SWF,      "swf",  null, "application/x-shockwave-flash",  null, "CWS", new String[]{"FWS"}, "Macromedia ShockWave Flash"),
498            // Adobe PDF: magic number as per: http://www.garykessler.net/library/file_sigs.html
499            new ExhibitTypeParameters(ET_PDF,      "pdf",  "application/pdf",  null, "%PDF", "Adobe Portable Document Format"),
500            // See RCF3839 at http://www.faqs.org/rfcs/rfc3839.html
501            new ExhibitTypeParameters(ET_3GP,      "3gp",  new String[]{"3gpp"}, "video/3gpp",  null, "", null, "3G mobile audio/video"),
502            // Windows Movie.
503            // Deduced magic from dump (od -tx1).
504            // 0000000 30 26 b2 75 8e 66 cf 11 a6 d9 00 aa 00 62 ce 6c
505            new ExhibitTypeParameters(ET_WMV,      "wmv",  null, "video/x-ms-wmv",  null, "0&", null, "Windows movie"),
506            // Windows Rich Text Format.
507            new ExhibitTypeParameters(ET_RTF,      "rtf",  null, "application/rtf",  null, "{\\rtf1", null, "Windows Rich Text Format"),
508            // Gallery tail mark-up language; opening line must be "@ " followed by title text.
509            // File content is strictly 7-bit, but may contain well-formed XHTML 1.0 markup including UNICODE character entities,
510            // ie should be suitable for insertion in XHTML and HTML UTF-8 documents.
511            new ExhibitTypeParameters(ET_TRML,     "trml", null, "text/x-pg2k-trail",  null, "@ ", null, "Gallery TRail Markup Language"),
512            // ZIP archive.  Magic number 50 4B 03 04 (PK..)
513            new ExhibitTypeParameters(ET_ZIP,      "zip", null, "application/zip",  null, "PK\u0003\u0004", null, "ZIP archive"),
514            // GZIPped tar archive.  Magic number 1F 8B 08
515            new ExhibitTypeParameters(ET_TGZ,      "tgz", null, "application/x-tgz",  null, "\u001f\u008b\u0008", null, "GZIPped tar archive"),
516            // BZIP2ed tar archive.  Magic number "BZh"
517            new ExhibitTypeParameters(ET_TBZ2,     "tbz2", null, "application/x-tbz2",  null, "BZh", null, "BZIP2ed tar archive"),
518            // Apple QuickTime movie.  No consistent magic number as such.
519            new ExhibitTypeParameters(ET_MOV,      "mov", null, "video/quicktime",  null, "", null, "QuickTime movie"),
520            };
521    
522        /**Sort in situ by type as needed. */
523        static { Arrays.sort(inputFileTypes); }
524    
525        /**Returns a (private) list of all valid exhibit types, in order by type. */
526        public static ExhibitTypeParameters[] getAllValidExhibitTypes()
527            { return(inputFileTypes.clone()); }
528    
529        /**Checks if the extension of the (putative) exhibit name is a recognised input extension.
530         * The string should not be null, nor start with a dot.
531         * (A null input gives a null return.)
532         * <p>
533         * This returns null if the extension is not recognised,
534         * else the (immutable) ExhibitTypeParameters describing it.
535         */
536        public static ExhibitTypeParameters isValidInputExhibitNameExtension(
537                                                            final CharSequence extension)
538            {
539            if(extension == null) { return(null); } // No extension at all!
540            final String extensionS = extension.toString();
541            final ExhibitTypeParameters result =
542                _iVIFE_cache.get(extensionS);
543            if(result != null) { return(result); } // All is well.
544            // Table might not have been initialised...
545            synchronized(_iVIFE_cache)
546                {
547                if(_iVIFE_cache.size() == 0)
548                    {
549                    for(int i = inputFileTypes.length; --i >= 0; )
550                        {
551                        _iVIFE_cache.put(inputFileTypes[i].suffixForInputFile,
552                                         inputFileTypes[i]);
553                        }
554                    return(_iVIFE_cache.get(extensionS));
555                    }
556                }
557            return(null); // Failed; not a valid extension.
558            }
559        /**Private cache of mappings from extension to parameters.
560         * Used by isValidInputExhibitNameExtension(), and filled in under
561         * protection of its own lock on first use if zero-sized.
562         * <p>
563         * Fixed small size.
564         */
565        private static final Hashtable<String,ExhibitTypeParameters> _iVIFE_cache =
566            new Hashtable<String, ExhibitTypeParameters>(inputFileTypes.length * 2 + 1);
567    
568        /**Gets type of an exhibit given its extension.
569         * The string passed must not be null, nor start with a dot.
570         * <p>
571         * This returns null if the extension is not recognised,
572         * else the (immutable) ExhibitTypeParameters describing the exhibit type.
573         * <p>
574         * This checks first by the primary suffix for each exhibit type,
575         * but if that fails may check by secondary suffix.
576         */
577        public static ExhibitTypeParameters getExhibitType(final String extension)
578            {
579            final ExhibitTypeParameters result =
580                _gET_cache.get(extension);
581            if(result != null) { return(result); } // All is well.
582            // Table might not have been initialised...
583            synchronized(_gET_cache)
584                {
585                if(_gET_cache.size() == 0)
586                    {
587                    for(int i = inputFileTypes.length; --i >= 0; )
588                        {
589                        _gET_cache.put(inputFileTypes[i].suffixForInputFile,
590                                       inputFileTypes[i]);
591                        }
592                    return(_gET_cache.get(extension));
593                    }
594                }
595            return(null); // Failed; not a valid extension.
596            }
597        /**Private cache of mappings from extension to parameters.
598         * Used by isValidInputExhibitNameExtension(), and filled in under
599         * protection of its own lock on first use if zero-sized.
600         * <p>
601         * Fixed small size.
602         */
603        private static final Hashtable<String,ExhibitTypeParameters> _gET_cache =
604            new Hashtable<String, ExhibitTypeParameters>(inputFileTypes.length * 2 + 1);
605    
606        /**Verifies the magic number for a given exhibit.
607         * This will try to read enough data to check the magic number.
608         * <p>
609         * Returns false if the magic number check fails.
610         * <p>
611         * If the file type specified has no unique magic number to
612         * be checked this always succeeds.
613         * <p>
614         * This function does not close the input stream.
615         *
616         * @param etp  is the exhibit type (must not be null)
617         * @param exhibitByteStream  is a stream of bytes from the
618         *     start of the exhibit at least long enough to contain the
619         *     magic number
620         */
621        public static boolean magicOK(final ExhibitTypeParameters etp,
622                                      final InputStream exhibitByteStream)
623            {
624            assert((etp != null) && (exhibitByteStream != null));
625    
626            // Now check the magic number if any...
627            final int numMagics = etp.allMagics.size();
628            if(numMagics == 0) { return(true); } // No magic to check...
629    
630            // Compute longest magic number/sequence for this file type.
631            int magicLength = etp.magic.length();
632            if(numMagics > 1)
633                {
634                for(final String s : etp.allMagics)
635                    { if(s.length() > magicLength) { magicLength = s.length(); } }
636                }
637    
638            final DataInputStream dis; // = null;
639            try
640                {
641                // OK, attempt to open the stream and read enough
642                // bytes to check the magic...
643                // (If the stream is not long enough we expect
644                // to get an EOFException thrown.)
645                dis = new DataInputStream(exhibitByteStream);
646                final byte data[] = new byte[magicLength];
647                dis.readFully(data);
648    
649                // If any magic matches then we are OK...
650                nextMagic: for(final String s : etp.allMagics)
651                    {
652                    for(int i = s.length(); --i >= 0; )
653                        {
654                        if(s.charAt(i) != (0xff & data[i]))
655                            { continue nextMagic; } // Match failed...
656                        }
657                    return(true); // Match succeeded on entire magic sequence...
658                    }
659                return(false); // Did not find a magic number to match.
660                }
661            catch(final IOException e) { return(false); } // Failed...
662            }
663    
664        /**Checks that file has expected magic number; throws IOException if not.
665         * @throws InvalidObjectException  if given file does not have
666         *     a correct magic number
667         * @throws IOException in case of difficulty reading the file
668         */
669        public static void checkMagicOK(final ExhibitTypeParameters etp, final File file)
670            throws IOException,
671                   InvalidObjectException
672            {
673            final FileInputStream fis = new FileInputStream(file);
674            try {
675                if(!ExhibitMIME.magicOK(etp, fis))
676                    { throw new InvalidObjectException("bad magic number: wrong file type"); }
677                }
678            finally { fis.close(); }
679            }
680    
681        /**The longest known magic number in bytes of any exhibit type supported; strictly positive once computed.
682         * Volatile to allow access without a lock.
683         * <p>
684         * Computed on first use.
685         */
686        private static volatile int longestMagicBytes;
687    
688        /**Get longest known magic number in bytes of any exhibit type supported; strictly positive. */
689        public static int getLongestMagicBytes()
690            {
691            if(longestMagicBytes == 0)
692                {
693                int longest = 0;
694                for(final ExhibitTypeParameters etp : inputFileTypes)
695                    {
696                    for(final String m : etp.allMagics)
697                        {
698                        final int mLen = m.length();
699                        if(mLen > longest) { longest = mLen; }
700                        }
701                    }
702                longestMagicBytes = longest;
703                assert(longestMagicBytes > 0);
704                }
705            return(longestMagicBytes);
706            }
707    
708    
709        /**Guesses the type of a file/exhibit from its magic number, or null if unrecognisable.
710         * This may only be able to check for files that have a unique magic number.
711         * <p>
712         * The stream may need to be markable to allow various possibilities
713         * to be tested.
714         */
715        public static ExhibitTypeParameters guessTypeFromMagic(final InputStream is)
716            throws IOException
717            {
718            if(is == null) { throw new IllegalArgumentException(); }
719    
720            // Read in enough bytes for the longest-known magic number
721            // if possible.
722            final byte buf[] = new byte[getLongestMagicBytes()];
723            int n = 0; // Bytes read so far.
724            while(n < buf.length)
725                {
726                final int r = is.read(buf, n, buf.length - n);
727                if(r < 1) { break; }
728                n += r;
729                }
730    
731            for(final ExhibitTypeParameters etp : inputFileTypes)
732                {
733                if(etp.allMagics.size() == 0)
734                    { continue; /* No magic number(s) to test for. */ }
735    
736                for(final String s : etp.allMagics)
737                    {
738                    final int mLen = s.length();
739                    if(mLen == 0)
740                        {
741                            if(mLen > n)
742                                { continue; /* File too short to be this type. */ }
743                            }
744                    boolean isOK = true;
745                    for(int i = mLen; --i >= 0; )
746                        {
747                        // A single mismatched byte means not this type...
748                        if(((byte) s.charAt(i)) != buf[i])
749                            {
750                            isOK = false;
751                            break;
752                            }
753                        }
754    
755                    // Return immediately if we found a match...
756                    if(isOK)
757                        { return(etp); }
758                    }
759                }
760    
761            return(null); // Couldn't recognise it.
762            }
763    
764        /**Gets the type of a file from its name, or null if unrecognised here.
765         * The string passed must not be null, nor start with a dot.
766         * <p>
767         * This returns null if the extension is not recognised for an exhibit,
768         * else returns the (immutable) ExhibitTypeParameters describing it.
769         * <p>
770         * The extension should be for an exhibit input,
771         * though this can be called to quickly check if a filename or URI
772         * might be for an exhibit, returning null if not.
773         *
774         * @param name  name of file or URL/URI
775         */
776        public static ExhibitTypeParameters getInputFileType(final CharSequence name)
777            {
778            return(isValidInputExhibitNameExtension(FileTools.getExtension(name)));
779            }
780    
781        /**Given an ET_XXX type, returns the ExhibitTypeParameters.
782         * Throws IllegalArgumentException for an illegal type.
783         */
784        public static synchronized ExhibitTypeParameters getParamsByType(final int type)
785            {
786            if(_gFPBT_cache == null)
787                {
788                _gFPBT_cache = new ExhibitTypeParameters[ET__max+1];
789                for(int i = inputFileTypes.length; --i >= 0; )
790                    {
791                    final ExhibitTypeParameters ifp = inputFileTypes[i];
792                    _gFPBT_cache[ifp.type] = ifp;
793                    }
794                }
795            if((type < ET__min) || (type > ET__max))
796                { throw new IllegalArgumentException("bad type"); }
797            final ExhibitTypeParameters result = _gFPBT_cache[type];
798            if(result == null)
799                { throw new IllegalArgumentException("bad type"); }
800            return(result);
801            }
802        /**Private cache of mappings from type to parameters.
803         * Constructed on first use by getParamsByType().
804         * <p>
805         * Fixed small size.
806         */
807        private static ExhibitTypeParameters _gFPBT_cache[];
808    
809        /**Get the MIME type of an exhibit (even at the end of filename or URI or URL) from its name; never null nor "".
810         */
811        public static String getMIMEType(final CharSequence name)
812            throws IOException
813            {
814            final String extension = FileTools.getExtension(name);
815            final ExhibitTypeParameters etp = getExhibitType(extension);
816            if(etp == null) // Can only happen if we stop recognising something already in the DB.
817                { throw(new IOException("unknown MIME type for ``"+extension+"''")); }
818            return(etp.mimeType);
819            }
820        }