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.InputStream;
034 import java.io.InvalidObjectException;
035 import java.io.ObjectInputValidation;
036 import java.io.Serializable;
037 import java.io.StringReader;
038 import java.util.Arrays;
039
040 import javax.xml.parsers.DocumentBuilder;
041 import javax.xml.parsers.DocumentBuilderFactory;
042 import javax.xml.parsers.ParserConfigurationException;
043
044 import org.hd.d.pg2k.svrCore.AllExhibitProperties.ExhibitDataSource;
045 import org.hd.d.pg2k.svrCore.MIME.ExhibitMIME;
046 import org.hd.d.pg2k.svrCore.MIME.Handler;
047 import org.w3c.dom.Document;
048 import org.w3c.dom.Node;
049 import org.xml.sax.InputSource;
050 import org.xml.sax.SAXException;
051
052 /**Immutable (and serialisable) store of all immutable computable auxiliary properties of a single exhibit.
053 * These are properties that can be computed from the basic exhibit binary
054 * data and that generally do not need to be recomputed
055 * (unless that exhibit changes,
056 * or if we upgrade the code and can get "better" values).
057 * <p>
058 * Some of the attributes contained in this object may be expensive to
059 * compute (and may require reading the entire exhibit)
060 * and thus this object is precomputed on the master server (or offline)
061 * where the exhibit is available from fast local storage in its entirety.
062 * <p>
063 * This is designed to be efficient on the wire and in memory, since these
064 * details will be held for each and every exhibit.
065 * <p>
066 * Some data in this object is transient and recomputable from other fields
067 * and is recomputed if this object is serialised and deserialised
068 * or is kicked out of memory due a shortage.
069 * <p>
070 * The equals() and hashCode() of this class can be very expensive,
071 * so intern()ing it can be too. However, we do intern() some of its elements.
072 * <p>
073 * The construction of this item is brittle, so that
074 * if we cannot construct it all, correctly, then we veto construction entirely,
075 * so as to avoid results from transient failures persisting indefinitely.
076 * <p>
077 * TODO: use a raw UTF form for the description text (not a String or Name) on the wire
078 * so that when serialising we don't create and hold onto another Object reference
079 * when we could instead have some compressed bytes in a stream somewhere
080 */
081 public final class ExhibitPropsComputable implements Serializable,
082 ObjectInputValidation,
083 MemoryTools.Internable,
084 MemoryTools.Compactable
085 {
086 /**Shared empty instance. */
087 public static final ExhibitPropsComputable EMPTY = new ExhibitPropsComputable();
088
089 /**Make an empty instance. */
090 private ExhibitPropsComputable() { this(null, null); }
091
092 /**Make non-empty instance.
093 * This does not intern() any of its arguments
094 * (so any that can usefully be intern()ed should already have been).
095 * <p>
096 * This may defensively copy mutable argument values.
097 *
098 * @param xyDim xy (positive) dimensions in pixels or null if none
099 * @param metadata normalised metadata else null if none.
100 */
101 private ExhibitPropsComputable(final Dimension xyDim,
102 final Object metadata)
103 {
104 // Take defensive copy if input if need be.
105 xyDimensions = (xyDim == null) ? null : new Dimension(xyDim);
106 this.metadata = metadata;
107
108 // Verify object state.
109 try { validateObject(); }
110 catch(final InvalidObjectException e)
111 { throw new IllegalArgumentException(e.getMessage(), e); }
112 }
113
114
115 /**Common "assumed" prefix on XML metadata. */
116 private static final String MDPREFIX = "<" + Handler.TAG_NAME_METADATA_TOP + ">";
117 private static final int MDPREFIX_LENGTH = MDPREFIX.length();
118 /**Common "assumed" suffix on XML metadata. */
119 private static final String MDSUFFIX = "</" + Handler.TAG_NAME_METADATA_TOP + ">";
120 private static final int MDSUFFIX_LENGTH = MDSUFFIX.length();
121
122 /**X,Y dimensions in pixels for images (moving or still) that have a fixed rectangular dimension, else null.
123 * If this is non-null then x and y are strictly positive.
124 * <p>
125 * Because Dimension is mutable, we hand out copies of it to callers.
126 */
127 private final Dimension xyDimensions;
128
129 /**Get x,y dimensions in pixels for images (moving or still) that have a fixed rectangular dimension, else null.
130 * If this is non-null then x and y are strictly positive.
131 * <p>
132 * We return a copy of our internal data.
133 */
134 public java.awt.Dimension getXyDimensions()
135 {
136 if(xyDimensions == null) { return(null); }
137 return(new Dimension(xyDimensions));
138 }
139
140 /**Shared static dictionary for use with in-memory Compact7BitString metadata.
141 * We expose this to help generate tuned dictionary values;
142 * this is safe to do since this dictionary is immutable.
143 */
144 public static final Compact7BitString.StaticDictionary sDict = new Compact7BitString.StaticDictionary("EPC",
145 // Most-common and largest early tokens first in list for maximum savings.
146 Arrays.asList(new String[]{
147 "NumProgressiveScans", /* count=8471, saving=152478, meanFirstPos=39 */
148 "ImageOrientation", /* count=9021, saving=135315, meanFirstPos=51 */
149 "javax_imageio_1", /* count=9058, saving=126812, meanFirstPos=3 */
150 "ColorSpaceType", /* count=9054, saving=117702, meanFirstPos=9 */
151 "NumChannels", /* count=9054, saving=90540, meanFirstPos=22 */
152 "Compression", /* count=9049, saving=90490, meanFirstPos=24 */
153 "Orientation", /* count=7724, saving=77240, meanFirstPos=96 */
154 "Dimension", /* count=9045, saving=72360, meanFirstPos=48 */
155 "Resolution", /* count=7783, saving=70047, meanFirstPos=108 */
156 "Lossless", /* count=9045, saving=63315, meanFirstPos=33 */
157 "TypeName", /* count=8477, saving=59339, meanFirstPos=27 */
158 "Chroma", /* count=9056, saving=45280, meanFirstPos=7 */
159 "normal", /* count=8433, saving=42165, meanFirstPos=53 */
160 "native", /* count=8232, saving=41160, meanFirstPos=64 */
161 "value", /* count=9206, saving=36824, meanFirstPos=17 */
162 "\"/></", /* count=9194, saving=36776, meanFirstPos=21 */
163 "image", /* count=9058, saving=36232, meanFirstPos=8 */
164 "false", /* count=8480, saving=33920, meanFirstPos=37 */
165 "YCbCr", /* count=8367, saving=33468, meanFirstPos=13 */
166 "Model", /* count=7697, saving=30788, meanFirstPos=83 */
167 "\"/><", /* count=9205, saving=27615, meanFirstPos=15 */
168 "name", /* count=9053, saving=27159, meanFirstPos=11 */
169 "JPEG", /* count=8433, saving=25299, meanFirstPos=30 */
170 "EXIF", /* count=8232, saving=24696, meanFirstPos=66 */
171 "Make", /* count=7437, saving=22311, meanFirstPos=72 */
172 "left", /* count=6913, saving=20739, meanFirstPos=101 */
173 "PixelAspectRatio", /* count=1236, saving=18540, meanFirstPos=56 */
174 "></", /* count=9019, saving=18038, meanFirstPos=61 */
175 "tag", /* count=8232, saving=16464, meanFirstPos=68 */
176 "DIGITAL", /* count=2422, saving=14532, meanFirstPos=86 */
177 "SONY", /* count=4230, saving=12690, meanFirstPos=75 */
178 "HorizontalScreenSize", /* count=574, saving=10906, meanFirstPos=77 */
179 "CompressionTypeName", /* count=572, saving=10296, meanFirstPos=31 */
180 "VerticalScreenSize", /* count=574, saving=9758, meanFirstPos=83 */
181 "F828", /* count=3215, saving=9645, meanFirstPos=87 */
182 "Canon", /* count=2383, saving=9532, meanFirstPos=75 */
183 "=\"", /* count=9206, saving=9206, meanFirstPos=12 */
184 "><", /* count=9206, saving=9206, meanFirstPos=3 */
185 "\" ", /* count=8545, saving=8545, meanFirstPos=72 */
186 "BackgroundIndex", /* count=572, saving=8008, meanFirstPos=21 */
187 "BitsPerSample", /* count=616, saving=7392, meanFirstPos=55 */
188 "SampleFormat", /* count=613, saving=6743, meanFirstPos=49 */
189 "HorizontalPixelSize", /* count=346, saving=6228, meanFirstPos=66 */
190 "BlackIsZero", /* count=620, saving=6200, meanFirstPos=16 */
191 "VerticalPixelSize", /* count=346, saving=5536, meanFirstPos=74 */
192 "stream", /* count=718, saving=3590, meanFirstPos=1 */
193 "Normal", /* count=588, saving=2940, meanFirstPos=76 */
194 "keyword", /* count=416, saving=2496, meanFirstPos=65 */
195 "comment", /* count=413, saving=2478, meanFirstPos=67 */
196 "Index", /* count=578, saving=2312, meanFirstPos=52 */
197 "SAMSUNG", /* count=337, saving=2022, meanFirstPos=70 */
198 "Data", /* count=616, saving=1848, meanFirstPos=46 */
199 "true", /* count=599, saving=1797, meanFirstPos=41 */
200 "TRUE", /* count=598, saving=1794, meanFirstPos=20 */
201 "Entry", /* count=415, saving=1660, meanFirstPos=63 */
202 "TECHWIN", /* count=260, saving=1560, meanFirstPos=69 */
203 "sampleRate", /* count=144, saving=1296, meanFirstPos=19 */
204 "sampleSize", /* count=144, saving=1296, meanFirstPos=31 */
205 "Text", /* count=417, saving=1251, meanFirstPos=61 */
206 "RGB", /* count=607, saving=1214, meanFirstPos=13 */
207 "frameSize", /* count=144, saving=1152, meanFirstPos=49 */
208 "lzw", /* count=574, saving=1148, meanFirstPos=35 */
209 "encoding", /* count=146, saving=1022, meanFirstPos=15 */
210 "channels", /* count=144, saving=1008, meanFirstPos=43 */
211 "sample", /* count=144, saving=720, meanFirstPos=37 */
212 "frames", /* count=143, saving=715, meanFirstPos=3 */
213 "audio", /* count=148, saving=592, meanFirstPos=11 */
214 "bytes", /* count=144, saving=576, meanFirstPos=53 */
215 "frame", /* count=144, saving=576, meanFirstPos=55 */
216 "unit", /* count=146, saving=438, meanFirstPos=21 */
217 "PCM_SIGNED", /* count=48, saving=432, meanFirstPos=17 */
218 "bits", /* count=144, saving=432, meanFirstPos=35 */
219 "8000", /* count=124, saving=372, meanFirstPos=27 */
220 "ULAW", /* count=91, saving=273, meanFirstPos=17 */
221 "GRAY", /* count=78, saving=234, meanFirstPos=13 */
222 "PaletteEntry", /* count=14, saving=154, meanFirstPos=33 */
223 "Hz", /* count=144, saving=144, meanFirstPos=23 */
224 "Palette", /* count=14, saving=84, meanFirstPos=31 */
225 "Gamma", /* count=19, saving=76, meanFirstPos=21 */
226 "green", /* count=14, saving=56, meanFirstPos=40 */
227 "index", /* count=14, saving=56, meanFirstPos=44 */
228 "PCM_UNSIGNED", /* count=5, saving=55, meanFirstPos=17 */
229 "22050", /* count=11, saving=44, meanFirstPos=27 */
230 "45453998", /* count=6, saving=42, meanFirstPos=27 */
231 "None", /* count=14, saving=42, meanFirstPos=36 */
232 "blue", /* count=14, saving=42, meanFirstPos=36 */
233 "39999998", /* count=5, saving=35, meanFirstPos=27 */
234 "44100", /* count=8, saving=32, meanFirstPos=27 */
235 "alpha", /* count=6, saving=24, meanFirstPos=35 */
236 "FALSE", /* count=5, saving=20, meanFirstPos=19 */
237 "CCITT", /* count=4, saving=16, meanFirstPos=36 */
238 "BI_RGB", /* count=3, saving=15, meanFirstPos=32 */
239 "45455", /* count=3, saving=12, meanFirstPos=27 */
240 "/><", /* count=4, saving=8, meanFirstPos=15 */
241 }));
242
243 /**Trimmed metadata for the exhibit (without the constant prefix/suffix); null if none available.
244 * This may exist in a number of formats for efficiency "on the wire"
245 * and to save space in memory.
246 * <p>
247 * <ul>
248 * <li>null: no metadata for this exhibit</li>
249 * <li>String: the simplest format, containing well-formed XML-format metadata (LEGACY)</li>
250 * <li>ROByteArray: deflated UTF-8 version of String format (LEGACY)</li>
251 * <li>Compact7BitString: compact byte representation (PREFERRED IN MEMORY)</li>
252 * <li>Name: compact byte representation (PREFERRED ON THE WIRE)</li>
253 * <li>byte[]: raw internal Compact7BitString data (LEGACY: NOT SUPPORTED)</li>
254 * </ul>
255 * <p>
256 * Note that this is stored without the redundant top-level tag.
257 * <p>
258 * Marked volatile to allow safe lockless update by compact().
259 */
260 private volatile Object metadata;
261
262 /**Get metadata as (immutable) trimmed XML, NOT surrounded with top-level tags; null if no metadata.
263 * @return null if no metadata
264 */
265 public CharSequence getMetadataAsXMLTrimmed()
266 {
267 // Capture local/fast snapshot.
268 final Object md = metadata;
269
270 if(null == md) // No metadata (rare, but legal).
271 { return(null); }
272
273 // Handle most common formats first.
274
275 // If a CharSequence then return as-is (assumed immutable).
276 if(md instanceof CharSequence) { return((CharSequence) md); }
277
278 if(md instanceof ROByteArray) // Deflated XML String.
279 {
280 try { return(ROByteArray.uncompressToString((ROByteArray) md)); }
281 catch(final IOException e) { throw new Error("invalid metadata", e); }
282 }
283
284 // Else use toString() (for Compact7BitString).
285 return(md.toString());
286 }
287
288 /**Get metadata as full XML string surrounded with top-level tags; null if no metadata.
289 * @return null if no metadata
290 */
291 public String getMetadataAsXML()
292 {
293 final CharSequence mdt = getMetadataAsXMLTrimmed();
294 if(null == mdt) { return(null); }
295 return(MDPREFIX + mdt + MDSUFFIX);
296 }
297
298 /**Get exhibit metadata; null if none available.
299 */
300 public synchronized Node getMetadata()
301 {
302 // If no metadata at all then immediately return null.
303 if(metadata == null) { return(null); }
304
305 Node result = null;
306 try { result = parseMetadata(getMetadataAsXML()); }
307 catch(final Exception e)
308 {
309 e.printStackTrace();
310 return(null); // Parse failed.
311 }
312
313 return(result);
314 }
315
316 /**Return a new factory each time to minimise chance of 'leaks'. */
317 private static DocumentBuilderFactory getFactory()
318 {
319 final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
320 factory.setValidating(false); // No validating (yet)...
321 factory.setNamespaceAware(false); // No namespaces (yet)...
322 factory.setIgnoringElementContentWhitespace(true); // Trim unnecessary whitespace.
323 return(factory);
324 }
325
326 /**Parse metadata from internal XML String format; never null.
327 *
328 * @param xml well-formed non-null non-empty XML metadata
329 * @return non-null DOM node
330 */
331 private static Node parseMetadata(final String xml)
332 throws IOException, SAXException, ParserConfigurationException
333 {
334 if((xml == null) || (xml.length() == 0))
335 { throw new IllegalArgumentException(); }
336
337 final DocumentBuilder builder = getFactory().newDocumentBuilder();
338
339 // Parse the XML input String...
340 final Document document = builder.parse(new InputSource(new StringReader(xml)));
341 final Node result = document.getFirstChild();
342
343 assert(Handler.TAG_NAME_METADATA_TOP.equals(result.getNodeName())) : "invalid top-level node";
344
345 return(result);
346 }
347
348 /**If true then attempt in-memory compression of metadata to save space with Compact7BitString. */
349 private static final boolean ATTEMPT_C7BS_COMPRESSION = true;
350
351 /**Attempt to compress supplied already-trimmed metadata; returns null if input is null.
352 * Given the metadata in full XML form,
353 * returns it trimmed, and compressed if possible.
354 */
355 private static Object _compressTrimmedMetadata(final CharSequence mdTrimmed)
356 {
357 if(mdTrimmed == null) { return(null); }
358
359 // We never expect to be handed an empty String.
360 assert(mdTrimmed.length() > 0);
361
362 // Compact7BitString pretty-much guarantees at least a x2 memory saving
363 // and is pretty light-weight on resources, so try it first.
364 //
365 // We use a private static dictionary to better compress metadata in memory.
366 // We choose this dictionary to work well for typical data in this class,
367 // but even a completely inappropriate dictionary does little harm
368 // relative to having no dictionary at all.
369 if(ATTEMPT_C7BS_COMPRESSION)
370 {
371 // If we encounter an error (eg non-ASCII data) just fall through.
372 try { return(MemoryTools.intern(Compact7BitString.convertToCompact7BitString(mdTrimmed, sDict))); }
373 catch(final Exception e) { /* Fall through if we cannot use this scheme. */ }
374 }
375
376 // No compression applicable: return as-is.
377 return(mdTrimmed);
378 }
379
380 /**Factory method to create a fully populated ExhibitPropsComputable object.
381 * This is given the a data source from which it can fetch
382 * the exhibit data one or more times to do its computations.
383 * <p>
384 * The data source object is not stored in the object.
385 * <p>
386 * This will veto any attempt at construction
387 * if the data object is not fully loaded,
388 * ie all available immediately with high bandwidth.
389 * <p>
390 * Underlying methods, such as getMetadata(), can throw all sorts of horrors,
391 * such as an Error, so we try very hard to fail gracefully if this happens
392 * and only throw an IOException.
393 */
394 public static ExhibitPropsComputable createExhibitPropsComputable(final ExhibitStaticAttr esa, final ExhibitDataSource ds) throws IOException
395 {
396 if((esa == null) || (ds == null)) { throw new IllegalArgumentException(); }
397
398 // Compute image X,Y dimensions, if applicable.
399 final ExhibitMIME.ExhibitTypeParameters et = (ExhibitMIME.getInputFileType(esa.getCharSequence()));
400
401 // If there is no handler for whatever reason,
402 // the give up trying to compute any details.
403 // (Though this should not really happen.)
404 if((et == null) || (et.handler == null)) { return(EMPTY); }
405
406 // Don't attempt to get any stream access if the exhibit is not fully loaded.
407 if(!ds.isExhibitFullyLoaded(esa))
408 { throw new IOException("exhibit not fully loaded"); }
409
410 final Dimension xyDimensions;
411 final InputStream is1 = ds.getInputStream(esa);
412 try { xyDimensions = et.handler.get2DImageDimensions(is1); }
413 finally { is1.close(); }
414
415 Object metadata = null;
416 final InputStream is2 = ds.getInputStream(esa);
417 try
418 {
419 // If we get no metadata back,
420 // or the top node has no children,
421 // then do not store any metadata.
422 final Node n = et.handler.getMetadata(is2, esa.getExhibitFullName());
423 if((n != null) && n.hasChildNodes())
424 {
425 // We assume the top-level tag is the correct one.
426 assert(Handler.TAG_NAME_METADATA_TOP.equals(n.getNodeName()));
427
428 final String mdString = TextUtils.toXML(n, false, true);
429 // Double-check that we can parse what we just got/generated...
430 assert(null != parseMetadata(mdString));
431
432 // Double-check that XML starts and ends with the top-level tag boilerplate.
433 assert(mdString.startsWith(MDPREFIX));
434 assert(mdString.endsWith(MDSUFFIX));
435
436 //if(ORG.hd.d.IsDebug.isDebug) { System.out.println("[ExhibitPropsComparable: raw metadata XML length "+mdString.length()+" for exhibit "+esa+".]"); }
437
438 // Create a version of this the metadata String with the boilerplate removed.
439 final String mdTrimmed = _trimStringFormOfOuterMetadataTag(mdString);
440
441 // If deferring compaction then don't attempt it here.
442 metadata = DEFER_COMPACTION ? mdTrimmed : _compressTrimmedMetadata(mdTrimmed);
443 }
444 }
445 catch(final SAXException e)
446 {
447 // A SAX error suggests that we are having difficulty (such as bad characters or quoting)
448 // with our XML text and thus should just pretend that we don't have any
449 // until we can fix the code and try again.
450 System.err.println("Trouble handling XML in extracting metadata for: " + esa.getCharSequence());
451 System.err.println("We regard this item as having no metadata for now, but correctly and completely computed.");
452 System.err.println("Try again when the code has been fixed!");
453 e.printStackTrace();
454 }
455 catch(final Throwable e)
456 {
457 // Note that we may get all manner of nasties thrown by metadata extractor routines...
458 // This includes things derived from Error and RuntimeException.
459 System.err.println("Trouble extracting metadata for: " + esa.getCharSequence());
460 e.printStackTrace();
461 final IOException e2;
462 if(e instanceof IOException) { e2 = (IOException) e; }
463 else
464 {
465 e2 = new IOException("Trouble extracting metadata for: " + esa.getCharSequence() + ": " + e.getMessage());
466 e2.initCause(e);
467 }
468 throw e2; // Veto construction; this item is not usable.
469 }
470 finally { is2.close(); }
471
472 // DHD20060815: We observe about half the metadata instances to be duplicates
473 // so we always try to intern() metadata for new instances.
474 return(new ExhibitPropsComputable(xyDimensions, MemoryTools.intern(metadata)));
475 }
476
477 /**Remove explicit fixed outer tag to save a little space, or null if input is null.
478 * Not for casual use, and not part of the supported API of this class.
479 */
480 public static String _trimStringFormOfOuterMetadataTag(final String mdString)
481 {
482 if(mdString == null) { return(null); }
483 return mdString.substring(MDPREFIX_LENGTH, mdString.length() - MDSUFFIX_LENGTH);
484 }
485
486 /**Our serial version... */
487 private static final long serialVersionUID = -8518817446313821597L;
488
489 /**Deserialise: use constructor for validation, defensive copying, etc.
490 * Also resolve all empty instances to a singleton as a minor optimisation,
491 * and immediately intern() new values so as to immediately discard
492 * duplicates (eg of exhibits already known) ASAP to minimise heap churn.
493 */
494 protected Object readResolve()
495 // throws ObjectStreamException
496 {
497 // Avoid duplicates of the empty instance.
498 if(equals(EMPTY)) { return(EMPTY); }
499
500 // We try to (re)normalise/compress the metadata upon loading
501 // if not in preferred compact and immutable form
502 // (and if we're not deferring by default and we have reasonable free memory
503 // to eagerly use CPU time when we might otherwise be blocking for I/O
504 // reading the serialised form off the wire for example).
505 // This on-the-fly compression can be quite expensive.
506 if(!(metadata instanceof Compact7BitString) &&
507 ((!DEFER_COMPACTION) || MemoryTools.lotsFree()))
508 {
509 return(new ExhibitPropsComputable(xyDimensions,
510 _compressTrimmedMetadata(getMetadataAsXMLTrimmed())));
511 }
512
513 // Construct new instance of object in normal defensive way.
514 // DHD20060815: I observe about half the metadata instances to be duplicates.
515 return(new ExhibitPropsComputable(xyDimensions, MemoryTools.intern(metadata)));
516 }
517
518 /**Serialise: write in the best format for the wire.
519 * To get best aggregate compressed size on the wire,
520 * eg where the compressed stream contains many similar non-identical instances,
521 * we always write out the metadata in its Name (or String) format (minus the outer tags)
522 * regardless of how it is actually held in memory.
523 * This also makes us immune to changes in the the internals of the other formats
524 * and allows use of a static dictionary with Compact7BitString for better in-memory compression
525 * (ie effectively cross-instance compression).
526 * <p>
527 * This allows a stream compressor to effectively remove the redundancy between
528 * instances of this class on the wire as well as internal redundancies.
529 * <p>
530 * We assume that there will almost never be identical instances on one stream
531 * so we don't mind writing new copies each time where it does happen.
532 */
533 protected Object writeReplace()
534 // throws ObjectStreamException
535 {
536 // Don't write multiple EMPTY instances.
537 if(this.equals(EMPTY)) { return(EMPTY); }
538
539 // If the metadata is null then write this instance as-is.
540 if(null == metadata) { return(this); }
541
542 // Return a Name-based instance for better inter-instance stream compression,
543 // and for a more stable serialised representation.
544 // We fall back to String representation if there is 8-bit data.
545 return(new ExhibitPropsComputable(xyDimensions,
546 Name.createOrStringFallback(getMetadataAsXMLTrimmed(), null)));
547 }
548
549 /**Validate fields/state.
550 * Called in the constructor and possibly after de-serialising.
551 * <p>
552 * Barf if something bad is found.
553 * (Maybe allow some extra info in debug version.)
554 */
555 public void validateObject()
556 throws InvalidObjectException
557 {
558 // Check that all components are sane and safe.
559 // If xyDimensions is set make sure that its values are OK.
560 if((xyDimensions != null) &&
561 ((xyDimensions.width <= 0) || (xyDimensions.height <= 0)))
562 { throw new InvalidObjectException("bad object: xyDimensions non-positive x,y"); }
563
564 if(metadata != null)
565 {
566 try
567 {
568 CharSequence s = null;
569 // Validate metadata in each legal representation.
570 if(metadata instanceof CharSequence)
571 { s = (CharSequence) metadata; }
572 else if(metadata instanceof Compact7BitString) // Compressed XML String.
573 { s = metadata.toString(); }
574 else if(metadata instanceof ROByteArray) // Compressed XML String.
575 { s = ROByteArray.uncompressToString((ROByteArray) metadata); }
576
577 if(s == null)
578 { throw new InvalidObjectException("bad object: metadata invalid format/type"); }
579 if(!TextUtils.startsWith(s, "<") || !TextUtils.endsWith(s, ">"))
580 { throw new InvalidObjectException("bad object: metadata not expected XML format"); }
581 if(TextUtils.startsWith(s, MDPREFIX) || TextUtils.endsWith(s, MDSUFFIX))
582 { throw new InvalidObjectException("bad object: invalid duplicate "+MDPREFIX+" outer tag"); }
583 }
584 catch(final Exception e)
585 {
586 e.printStackTrace();
587 throw new InvalidObjectException("bad object: cannot parse XML metadata");
588 }
589 }
590 }
591
592 /**Equal if all the members are. */
593 @Override public boolean equals(final Object obj)
594 {
595 if(this == obj) { return(true); }
596 if(!(obj instanceof ExhibitPropsComputable)) { return(false); }
597 final ExhibitPropsComputable other = (ExhibitPropsComputable) obj;
598 if(null == xyDimensions)
599 { if(null != other.xyDimensions) { return(false); } }
600 else if(!xyDimensions.equals(other.xyDimensions)) { return(false); }
601
602 // Compare the metadata in full (expensively)...
603 if(null == metadata) { return(null == other.metadata); }
604 else if(null == other.metadata) { return(false); }
605 return(TextUtils.contentEquals(getMetadataAsXMLTrimmed(), other.getMetadataAsXMLTrimmed()));
606 }
607
608 /**Human-readable summary. */
609 @Override public String toString()
610 {
611 final StringBuilder sb = new StringBuilder();
612 sb.append("<ExhibitPropsComputable");
613 final String md = getMetadataAsXML();
614 if(md != null) { sb.append(":metadata=").append(md); }
615 if(xyDimensions != null) { sb.append(":xyDimensions=").append(xyDimensions); }
616 sb.append(">");
617 return(sb.toString());
618 }
619
620 /**Private cache of the hash value; initially zero, and zero for an EMPTY instance.
621 * Thread-safe as int access guaranteed atomic.
622 */
623 private transient int _hash;
624
625 /**Compute the hash value.
626 * As well as being used for hash tables,
627 * this will be used to detect changes in the representation of computed data,
628 * ie for the overall AEP hash,
629 * so this should include all significant elements.
630 * <p>
631 * Note that this also means that the AEP hash
632 * contains a semantically-significant sampling of the exhibit data
633 * as well as such things as length and timestamp from the AEID.
634 */
635 private int _computeHash()
636 {
637 // Compute the x,y dimension component of the hash...
638 // Popular formats such as GIF and JPEG are limited to 2^16 pixels per side,
639 // and indeed most such images are well below this limit,
640 // so using a prime multiplier a little below this for one dimension
641 // and adding to the other should achieve a reasonable hash spread.
642 final int h1;
643 if(null != xyDimensions)
644 { h1 = xyDimensions.width ^ (xyDimensions.height * 65521); }
645 else
646 { h1 = 0; }
647
648 // Compute the metadata component of the hash...
649 if(null == metadata) { return(h1); }
650 // If in the (trimmed) String format internally then use its hash directly for efficiency.
651 if(metadata instanceof String) { return(h1 ^ metadata.hashCode()); }
652 // Else compute the String hash of the synthesised trimmed XML String.
653 return(h1 ^ getMetadataAsXMLTrimmed().toString().hashCode());
654 }
655
656 /**Hash based on the dimensions, else on the metadata. */
657 @Override public int hashCode()
658 {
659 int h;
660 if((h = _hash) == 0)
661 { _hash = h = _computeHash(); /* Cache the hash value for speed. */ }
662 return(h);
663 }
664
665 /**If true then defer compaction of metadata.
666 * If true then (any) compaction is <em>NOT</em> done
667 * during construction or deserialisation
668 * since it may place memory under more stress
669 * with old and new versions in memory simultaneously,
670 * but until some later point such as a call to compact().
671 * <p>
672 * Conversely, this could save a lot of time constructing/deserialising data
673 * before the first operations can be performed on it
674 * and where memory space is not the primary constraint.
675 */
676 private static final boolean DEFER_COMPACTION = true;
677
678 /**Compact the internal representation of this instance (and its sub-objects) if possible.
679 * This has no effect on the logical content of this instance in-memory or serialised,
680 * is guaranteed to be safe to run concurrently with other uses of this instance
681 * (and will take any locks as needed to work incrementally),
682 * and may do nothing but consume some CPU cycles.
683 * <p>
684 * This may be able to convert some state to a more memory-efficient representation
685 * after construction or deserialisation,
686 * and is suitable to be called by a background thread.
687 * <p>
688 * We don't prevent multiple concurrent calls to this routine,
689 * since they are at worst wasteful of CPU but not unsafe.
690 */
691 public void compact()
692 {
693 if(!DEFER_COMPACTION) { return; /* Nothing to do; must already be compact. */ }
694
695 final Object md = metadata; // Capture snapshot.
696
697 // If already in preferred compact form (or null) then intern/return.
698 if(md == null) { return; /* Nothing to do. */ }
699 if(md instanceof Compact7BitString) { return; /* Nothing to do. */ }
700
701 // Replace metadata with intern()ed/compact form if possible.
702 // DHD20060815: We observe about half the metadata instances to be duplicates.
703 metadata = _compressTrimmedMetadata(getMetadataAsXMLTrimmed());
704 }
705
706 /**Get name of this Compactable instance for tracking purposes, or null if none. */
707 public String getCompactableInstanceName() { return("EPC|"+System.identityHashCode(this)); }
708 }