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;
030    
031    import java.awt.Dimension;
032    import java.io.IOException;
033    import java.io.InvalidObjectException;
034    import java.io.ObjectInputStream;
035    import java.io.ObjectInputValidation;
036    import java.io.ObjectStreamField;
037    import java.io.OutputStream;
038    import java.io.Serializable;
039    import java.security.MessageDigest;
040    import java.security.NoSuchAlgorithmException;
041    import java.util.Arrays;
042    import java.util.Date;
043    
044    /**Immutable, Serializable thumbnails/samples of an exhibit.
045     * This is designed to be transportable across a network if need be,
046     * and persistable to disc, and reasonably efficient when serialised.
047     * <p>
048     * This holds a standard and a small thumbnail (of a static image)
049     * or equivalent sample of an exhibit for other exhibit types,
050     * or an indication that such a sample is not needed because the original can be used,
051     * or an indication that such a sample could not be made (eg failed and should not be rebuilt).
052     * <p>
053     * Exhibit thumbnails/samples are strictly limited in size to minimise
054     * the space they consume in toto and the time that they take to send across
055     * the network to clients.  The small thumbnails are meant to be small
056     * enough that they can be permanently held in memory at the servers
057     * and are lightning-fast to transfer to client (while not so small
058     * as to simple dwarfed by network overheads).
059     * <p>
060     * (All thumbnails/samples must be smaller than the original exhibit.)
061     * <p>
062     * Any sample is of the same MIME type as the original.
063     * <p>
064     * An example of when an exhibit could be used as its own thumbnail is
065     * when its size in bytes is less than the appropriate limit, and the (x,y)
066     * dimensions are no greater than the appropriate limits, then no thumbnail
067     * is required and the image can be used directly.
068     * <p>
069     * This is made to implement equals() and hashCode() so that
070     * comparisons for equality, eg in a Set, can be performed.
071     * Two objects are equal if their constituent thumbnails are equal.
072     * The hash is computed from those of the thumbnails.
073     */
074    public final class ExhibitThumbnails implements Serializable, ObjectInputValidation
075        {
076        /**Value indicating that no thumbnails could be made for an exhibit.
077         * This non-null value positively indicates that there are no thumbnails.
078         * <p>
079         * This value is a singleton.
080         */
081        public static final ExhibitThumbnails NO_THUMBNAILS =
082            new ExhibitThumbnails(null, null);
083    
084        /**Returns true if its argument is null or contains no thumbnails.
085         */
086        public static boolean isEmpty(final ExhibitThumbnails tns)
087            {
088            if(tns == null) { return(true); }
089            return((tns.getSmall() == null) && (tns.getStandard() == null));
090            }
091    
092        /**Small thumbnail; null means none can be constructed. */
093        private final Thumbnail sml;
094    
095        /**Get small thumbnail; null means none can be constructed. */
096        public Thumbnail getSmall() { return(sml); }
097    
098        /**Standard thumbnail; null means none can be constructed. */
099        private final Thumbnail std;
100    
101        /**Get standard thumbnail; null means none can be constructed. */
102        public Thumbnail getStandard() { return(std); }
103    
104        /**Creation timestamp (positive), or zero if none. */
105        public final long created;
106    
107        /**Construct from a pair of thumbnails.
108         * Either can be null or non-null, but if both are non-null
109         * small.size() must not be greater than standard.size().
110         * <p>
111         * The timestamp is always zero if both thumbnails are.
112         */
113        private ExhibitThumbnails(final Thumbnail small, final Thumbnail standard)
114            {
115            sml = small;
116            std = standard;
117            created = ((small == null) && (standard == null)) ? 0 : System.currentTimeMillis();
118    
119            // Verify what we've been given.
120            try { validateObject(); }
121            catch(final InvalidObjectException e)
122                { throw new IllegalArgumentException(e.getMessage()); }
123            }
124    
125        /**Absolute maximum size of any standard thumbnail/sample binary in bytes (32kB-1).
126         * Should be small enough to download reasonably quickly,
127         * but may be too large to cache in memory.
128         * <p>
129         * This value is small enough to store in a short.
130         */
131        public static final short STD_ABS_MAX_BYTES = 0x7fff;
132    
133        /**Absolute maximum size of any small thumbnail/sample binary in bytes (4kB-1).
134         * Should be very fast to download, even over a WAN connection,
135         * and small enough to cache all exhibits' small thumbnails in memory if required.
136         * <p>
137         * This value is small enough to store in a short.
138         */
139        public static final short SML_ABS_MAX_BYTES = 0xfff;
140    
141    
142        /**For a standard static image thumbnail, its longest dimension in pixels.
143         * Normally the longest dimension will be exactly this in pixels,
144         * but it may be smaller.
145         */
146        public static final int STD_STATIC_IMAGE_TN_LDIM_PX = 256;
147    
148        /**For a small static image thumbnail, its longest dimension in pixels.
149         * Normally the longest dimension will be exactly this in pixels,
150         * but it may be smaller.
151         */
152        public static final int SML_STATIC_IMAGE_TN_LDIM_PX = 64;
153    
154        /**Crude estimate of maximum bytes consumed in memory and on disc by thumbnails; ignores VM and serialisation details, etc. */
155        public static final int MAX_BYTES_EST = STD_ABS_MAX_BYTES + SML_ABS_MAX_BYTES + 1024;
156    
157        /**Returns a hash code value for the object.
158         * This is based on the hashes of the constituent thumbnails.
159         */
160        @Override
161        public int hashCode()
162            {
163            // Uses unique (different, small-prime) values for missing thumbnails.
164            final int smlHC = ((sml == null) ? 3 : sml.hashCode());
165            // We work a bit harder on the std thumbnail value as we assume it
166            // will be missing more often and so we will pay a slightly lower
167            // cost than it would otherwise appear.
168            final int stdHC = ((std == null) ? 5 : (17 * std.hashCode()));
169            return(smlHC ^ stdHC);
170            }
171    
172        /**Indicates whether some other object is "equal to" this one.
173         * For this the constituent thumbnails must be "equal",
174         * and it means that iff one of the thumbnails is missing in one
175         * of the objects then it must be missing in the other too.
176         * <p>
177         * Equality ignores the creation timestamp.
178         */
179        @Override
180        public boolean equals(final Object obj)
181            {
182            if(obj == this) { return(true); } // Will be reasonably common case.
183            if(!(obj instanceof ExhibitThumbnails)) { return(false); }
184            final ExhibitThumbnails other = (ExhibitThumbnails) obj;
185    
186            // Possibly quicker to test equality on smaller thumbnail first.
187            final boolean smlIsSame = (sml == null) ?
188                (other.sml == null) :
189                (sml.equals(other.sml));
190            if(!smlIsSame) { return(false); }
191    
192            final boolean stdIsSame = (std == null) ?
193                (other.std == null) :
194                (std.equals(other.std));
195            if(!stdIsSame) { return(false); }
196    
197            return(true); // Seem to be the same!
198            }
199    
200        /**Given the original dimensions of an image, compute the size of the standard/small thumbnail.
201         * The argument must be non-null and the width and height strictly positive.
202         * <p>
203         * If the longest dimension of the original image is no more than
204         * {STD|SML}_STATIC_IMAGE_TN_LDIM_PX pixels then the input dimensions will
205         * be returned unaltered, else the values are scaled so that the
206         * longest dimension is exactly {STD|SML}_STATIC_IMAGE_TN_LDIM_PX pixels.
207         *
208         * @param std  if true, compute for standard, else compute for small
209         */
210        public static Dimension computeThumbnailDimensions(final Dimension original,
211                                                           final boolean std)
212            {
213            if(original == null) { throw new IllegalArgumentException(); }
214            final int w = original.width;
215            final int h = original.height;
216            if((w <= 0) || (h <= 0)) { throw new IllegalArgumentException(); }
217    
218            final int limit = std ? STD_STATIC_IMAGE_TN_LDIM_PX : SML_STATIC_IMAGE_TN_LDIM_PX;
219    
220            final int longestDim = Math.max(w, h);
221            if(longestDim <= limit) { return(original); }
222    
223            final Dimension result = new Dimension();
224            result.width  = (w >= h) ? limit : Math.min(limit, (w * limit + longestDim/2) / longestDim);
225            result.height = (h >= w) ? limit : Math.min(limit, (h * limit + longestDim/2) / longestDim);
226    
227            assert Math.max(result.width, result.height) <= limit;
228    
229            return(result);
230            }
231    
232    
233        /**My initial version number. */
234        private static final long serialVersionUID = 2686345754458313314L;
235    
236        /**Deserialise. */
237        private void readObject(final ObjectInputStream in)
238            throws IOException, ClassNotFoundException
239            {
240            in.defaultReadObject();
241            validateObject(); // Validate state immediately.
242            }
243    
244        /**Eliminate duplicate empty objects after deserialisation. */
245        protected Object readResolve()
246            {
247            if(NO_THUMBNAILS.equals(this))
248                { return(NO_THUMBNAILS); }
249            return(this);
250            }
251    
252        /**Validate fields/state.
253         * Called in the constructor and possibly after deserialising.
254         * <p>
255         * Barf if something bad is found.
256         * (Maybe allow some extra info in debug version.)
257         */
258        public void validateObject()
259            throws InvalidObjectException
260            {
261            // Check that all components are sane and safe.
262            // We check that the small thumbnail is no larger than the standard one.
263            // We should probably also check thumbnail image dimensions...
264            if((std != null) && (sml != null) && (sml.size() > std.size()))
265                { throw new InvalidObjectException("bad object: sml is larger than std"); }
266    
267            // Check the harsher constraints on the small thumbnail.
268            if(sml != null)
269                {
270                // The data must not be too large.
271                if(sml.size() > SML_ABS_MAX_BYTES)
272                    { throw new InvalidObjectException("bad object: small thumbnail data too large"); }
273    
274                // xyDim, if present, must have strictly positive components.
275                final Dimension xyDim = sml.getXyDim();
276                if(xyDim != null)
277                    {
278                    if((xyDim.width > SML_STATIC_IMAGE_TN_LDIM_PX) || (xyDim.height > SML_STATIC_IMAGE_TN_LDIM_PX))
279                        { throw new InvalidObjectException("bad object: small thumbnail xyDim width or height too large"); }
280                    }
281                }
282    
283            // If the timestamp is not absent (zero) then it must be positive.
284            if(created < 0)
285                { throw new InvalidObjectException("bad object: invalid timestamp"); }
286            if((sml == null) && (std == null) && (created != 0))
287                { throw new InvalidObjectException("bad object: invalid non-zero timestamp with no thumbnail data"); }
288            }
289    
290        /**Make a pair of thumbnails.
291         * Either can be null or non-null, but if both are non-null
292         * small.size() must not be greater than standard.size(),
293         * and both must be within their respective size limits.
294         */
295        public static ExhibitThumbnails createExhibitThumbnails(final Thumbnail small, final Thumbnail standard)
296            {
297            // Avoid redundant creation of multiple "empty" placeholders.
298            if((small == null) && (standard == null)) { return(NO_THUMBNAILS); }
299    
300            return(new ExhibitThumbnails(small, standard));
301            }
302    
303    
304        /**Immutable, Serializable single thumbnail (standard or small).
305         * Implements hashCode() and equals(),
306         * with equals() meaning dimensional and byte-for-byte equality.
307         */
308        public static final class Thumbnail implements Serializable, ObjectInputValidation
309            {
310            /**Construct an image thumbnail with x,y dimensions and a byte array.
311             * The byte array must not be null nor zero-length.
312             * The Dimension object must be non-null and the width and height
313             * must be strictly positive.
314             * <p>
315             * Both arguments are copied and are not altered.
316             */
317            public Thumbnail(final byte _data[], final Dimension _xyDim)
318                {
319                data = _data.clone(); // Defensive copy.
320                xyDim = (_xyDim == null) ? null : new Dimension(_xyDim); // Defensive copy.
321                hashMD5 = _computeMD5Hash(data); // Compute new copy.
322    
323                // Verify what we've been given.
324                try { validateObject(); }
325                catch(final InvalidObjectException e)
326                    { throw new IllegalArgumentException(e); }
327                }
328    
329            /**Mark all mutable members as unshared for safety. */
330            private static final ObjectStreamField[] serialPersistentFields = {
331                new ObjectStreamField("data", byte[].class, true),
332                new ObjectStreamField("xyDim", Dimension.class, true),
333                new ObjectStreamField("hashMD5", byte[].class, true),
334                };
335    
336            /**Thumbnail raw data; non-null, non-zero-length. */
337            private final byte data[];
338    
339            /**Image width and height for image thumbnail, both strictly positive or whole object null. */
340            private final Dimension xyDim;
341    
342            /**The MD5 hash of the thumbnail data (16 bytes), or null.
343             * This is only present (non-null) if computed at source/construction.
344             * <p>
345             * This may also be null when deserialised from an old instance
346             * before we computed and stored this value.
347             */
348            private final byte hashMD5[];
349    
350            /**Return size of data array. */
351            public int size() { return(data.length); }
352    
353            /**Get data as a (copied) byte[]. */
354            public byte[] toByteArrray() { return(data.clone()); }
355    
356            /**Write data to a stream, avoiding an unnecessary copy in many cases.
357             * We trust the os.write() method(s) not to alter
358             * the data that we pass to it.
359             *
360             * @param os  (safe) OutputStream on which to write the binary data;
361             *     never null
362             * @throws IOException  if thrown by os
363             */
364            public void writeData(final OutputStream os)
365                throws IOException
366                { os.write(data); }
367    
368            /**Get XxY dimension of thumbnail, or null if not appropriate. */
369            public Dimension getXyDim()
370                {
371                if(xyDim == null) { return(null); }
372                return(new Dimension(xyDim));
373                }
374    
375            /**The hash code is based on things we can compute very quickly.
376             * For this we use:
377             * <ul>
378             * <li>The byte-for-byte size of the thumbnail data.
379             * <li>The first and mid-point bytes of the thumbnail data
380             *     (we know there must be at least one byte,
381             *     and they may contain a magic number and something interesting).
382             * </ul>
383             * <p>
384             * We could use the height and width, but we assume that they may
385             * not be very effective in distinguishing between thumbnails as
386             * they may be constrained to be fixed in one dimension or the other.
387             */
388            @Override
389            public int hashCode()
390                {
391                return((data.length * 65327) +
392                       ((data[0]) * 257) + (data[data.length / 2]));
393                }
394    
395            /**For equality the thumbnails must be indistinguishable.
396             * That is, they must be the same dimensions
397             * and their data length must be the same
398             * and the data must match byte for byte.
399             */
400            @Override
401            public boolean equals(final Object obj)
402                {
403                if(this == obj) { return(true); }
404                if(!(obj instanceof Thumbnail)) { return(false); }
405                final Thumbnail other = (Thumbnail) obj;
406    
407                // Check data length first,
408                // as this is quick to do and should have a good spread of values,
409                // ie should distinguish most non-identical thumbnails.
410                if(data.length != other.data.length) { return(false); }
411    
412                // Check for different dimensions.
413                if(xyDim != null)
414                    { if(!xyDim.equals(other.xyDim)) { return(false); } }
415                else
416                    { if(other.xyDim != null) { return(false); } }
417    
418                // Now try a byte-for-byte comparison, which may be very expensive.
419                return(Arrays.equals(data, other.data));
420                }
421    
422            /**Human-readable summary. */
423            @Override
424            public String toString()
425                {
426                final StringBuilder sb = new StringBuilder(64);
427                sb.append("tn:data.length=").append(data.length);
428                if(xyDim != null) { sb.append(':').append(xyDim); }
429                return(sb.toString());
430                }
431    
432            /**True iff this thumbnail has a precomputed MD5hash stored.
433             * If true then getMD5Hash() does not actually have to calculate the hash.
434             * <p>
435             * If false then this instance has reduced protection against data corruption.
436             */
437            public boolean hasMD5Hash()
438                {
439                return(hashMD5 != null);
440                }
441    
442            /**Get the MD5 hash of the thumbnail data; never null.
443             * If this is stored in the object then we return a copy to the caller,
444             * else we return the result of computing it afresh on the data.
445             *
446             * @return a private copy of the MD5 hash bytes; never null
447             */
448            public byte[] getMD5Hash()
449                {
450                if(hashMD5 != null) { return(hashMD5.clone()); }
451                return(_computeMD5Hash(data));
452                }
453    
454            /**Compute the MD5 hash of the supplied data; never null.
455             * Computed on demand every time.
456             */
457            private byte[] _computeMD5Hash(final byte[] d)
458                {
459                final MessageDigest hMD5;
460                try { hMD5 = MessageDigest.getInstance(CoreConsts.HASH_MD5); }
461                catch(final NoSuchAlgorithmException e) // Should never happen...
462                    { throw new Error("could not find "+CoreConsts.HASH_MD5+" digester!"); }
463                hMD5.update(d); // Digest data all in one go.
464                return(hMD5.digest());
465                }
466    
467            /**Unique serialisation version number. */
468            private static final long serialVersionUID = -1279061480258712366L;
469    
470            /**Deserialise. */
471            private void readObject(final ObjectInputStream in)
472                throws IOException, ClassNotFoundException
473                {
474                in.defaultReadObject(); // Note that mutable fields have been marked as unshared.
475                validateObject(); // Validate state immediately.
476                }
477    
478            /**Validate fields/state.
479             * Called in the constructor and possibly after deserialising.
480             * <p>
481             * This only checks that this is good as a standard thumbnail:
482             * extra checking may have to be imposed elsewhere for a small
483             * thumbnail.
484             * <p>
485             * Barf if something bad is found.
486             * (Maybe allow some extra info in debug version.)
487             */
488            public void validateObject()
489                throws InvalidObjectException
490                {
491                // Check that all components are sane and safe.
492    
493                // The data must be non-zero length.
494                if((data == null) || (data.length == 0))
495                    { throw new InvalidObjectException("bad object: data null or zero length"); }
496                // The data must not be too large.
497                if(data.length > STD_ABS_MAX_BYTES)
498                    { throw new InvalidObjectException("bad object: data too large"); }
499    
500                // xyDim, if present then it must have strictly positive components.
501                if(xyDim != null)
502                    {
503                    if((xyDim.width <= 0) || (xyDim.height <= 0))
504                        { throw new InvalidObjectException("bad object: xyDim width or height non-positive"); }
505                    if((xyDim.width > STD_STATIC_IMAGE_TN_LDIM_PX) || (xyDim.height > STD_STATIC_IMAGE_TN_LDIM_PX))
506                        { throw new InvalidObjectException("bad object: xyDim width or height too large"); }
507                    }
508                // Check combinations of values.
509                // For the moment, xyDim must always be non-null.
510                else
511                    { throw new InvalidObjectException("bad object: xyDim is null"); }
512    
513                // If the hash is present then it must match the data.
514                if(hashMD5 != null)
515                    {
516                    if(!Arrays.equals(hashMD5, _computeMD5Hash(data)))
517                        { throw new InvalidObjectException("bad object: data corrupt; does not match hash"); }
518                    }
519                }
520            }
521    
522        /**Human-readable summary. */
523        @Override
524        public String toString()
525            {
526            final StringBuilder sb = new StringBuilder(128);
527            sb.append("thumbnails");
528            if((sml == null) && (std == null)) { sb.append(":EMPTY"); }
529            else
530                {
531                if(sml != null) { sb.append(":sml:").append(sml); }
532                if(std != null) { sb.append(":std:").append(std); }
533                }
534            if(created != 0) { sb.append(":created=").append(new Date(created)); }
535            return(sb.toString());
536            }
537        }