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 }