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        }