001    /*
002    Copyright (c) 1996-2012, 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    
030    package org.hd.d.pg2k.webSvr.threeD;
031    
032    import java.awt.image.BufferedImage;
033    import java.io.ByteArrayInputStream;
034    import java.io.ByteArrayOutputStream;
035    import java.io.FileNotFoundException;
036    import java.io.IOException;
037    import java.io.InputStream;
038    import java.io.OutputStream;
039    import java.lang.ref.SoftReference;
040    import java.net.HttpURLConnection;
041    import java.net.MalformedURLException;
042    import java.net.URL;
043    import java.net.URLConnection;
044    import java.util.ArrayList;
045    import java.util.BitSet;
046    import java.util.Hashtable;
047    import java.util.List;
048    import java.util.Map;
049    import java.util.Timer;
050    import java.util.TimerTask;
051    
052    import javax.jnlp.BasicService;
053    import javax.jnlp.FileContents;
054    import javax.jnlp.PersistenceService;
055    import javax.jnlp.ServiceManager;
056    import javax.jnlp.SingleInstanceService;
057    import javax.jnlp.UnavailableServiceException;
058    import javax.media.j3d.Texture;
059    
060    import org.hd.d.pg2k.svrCore.AllExhibitImmutableData;
061    import org.hd.d.pg2k.svrCore.AllExhibitProperties;
062    import org.hd.d.pg2k.svrCore.CoreConsts;
063    import org.hd.d.pg2k.svrCore.ExhibitName;
064    import org.hd.d.pg2k.svrCore.ExhibitStaticAttr;
065    import org.hd.d.pg2k.svrCore.ExhibitThumbnails;
066    import org.hd.d.pg2k.svrCore.ImageUtils;
067    import org.hd.d.pg2k.svrCore.Name;
068    import org.hd.d.pg2k.svrCore.Name.ExhibitFull;
069    import org.hd.d.pg2k.svrCore.ROByteArray;
070    import org.hd.d.pg2k.svrCore.Rnd;
071    import org.hd.d.pg2k.svrCore.SimpleLoggerIF;
072    import org.hd.d.pg2k.svrCore.MIME.ExhibitMIME;
073    import org.hd.d.pg2k.svrCore.collections.SimpleLRUMap;
074    import org.hd.d.pg2k.svrCore.datasource.ExhibitDataFileSource;
075    
076    import ORG.hd.d.IsDebug;
077    
078    import com.sun.j3d.utils.image.TextureLoader;
079    
080    
081    /**3D Walkthrough "business-logic" holder.
082     * Is designed to be GUI-free and just contain the logic.
083     * <p>
084     * Package visible since need be seen only by the main GUI class.
085     */
086    final class ThreeDLogic implements LightweightMetaDataFetchInterface
087        {
088        /**Package-visible constructor since need be seen only by the main GUI class.
089         * @param logger  reference to central logger; never null
090         */
091        ThreeDLogic(final SimpleLoggerIF logger)
092            {
093            assert(logger != null);
094            this.logger = logger;
095    
096            // Get access to basic services.
097            BasicService basicService = null;
098            try { basicService = (BasicService) ServiceManager.lookup("javax.jnlp.BasicService"); }
099            catch(final UnavailableServiceException e) { }
100            bs = basicService;
101    
102            // Get access to persistence management.
103            PersistenceService persistenceService = null;
104            try { persistenceService = (PersistenceService) ServiceManager.lookup("javax.jnlp.PersistenceService"); }
105            catch(final UnavailableServiceException e) { }
106            ps = persistenceService;
107    
108    //        // Get access to file open services.
109    //        FileOpenService fileOpenService = null;
110    //        try { fileOpenService = (FileOpenService) ServiceManager.lookup("javax.jnlp.FileOpenService"); }
111    //        catch(final UnavailableServiceException e) { }
112    //        fos = fileOpenService;
113    
114            // Get access to single-instance services.
115            SingleInstanceService singleInstanceService = null;
116            try { singleInstanceService = (SingleInstanceService) ServiceManager.lookup("javax.jnlp.SingleInstanceService"); }
117            catch(final UnavailableServiceException e) { }
118            sis = singleInstanceService;
119    //
120    //        // Get access to extended services.
121    //        ExtendedService extendedService = null;
122    //        try { extendedService = (ExtendedService) ServiceManager.lookup("javax.jnlp.ExtendedService"); }
123    //        catch(final UnavailableServiceException e) { }
124    //        exs = extendedService;
125    
126            if((ps != null) && (bs != null))
127                {
128                // Try to reload the properties/preferences...
129                // This should be quick,
130                // and doing it here should avoid races with the UI.
131    //            try
132    //                {
133    //                final FileContents fc = ps.get(makePropsURL(bs));
134    //                final InputStream inputStream = fc.getInputStream();
135    //                try { props.loadFromPersistentData(inputStream); }
136    //                finally { inputStream.close(); }
137    //                }
138    //            catch(final IOException e)
139    //                {
140    //                logger.log("No saved properties/preferences to reload (yet).");
141    //                }
142                }
143            else
144                {
145                // If there is no persistence...
146                }
147    
148    
149            if(ALLOW_FALLBACK_FS_FILE_ACCESS)
150                {
151                logger.log("Allowing fallback filesystem access...");
152    
153                // If we are not running in JWS
154                // then try to pre-load a test exhibit set from the filesystem.
155                if(bs == null)
156                    {
157                    final ExhibitDataFileSource edfs = new ExhibitDataFileSource(logger);
158                    try
159                        {
160                        final AllExhibitImmutableData aeid = edfs.getAllExhibitImmutableData(-1);
161                        final List<ExhibitFull> allExhibitNamesSorted = aeid.getAllExhibitNamesSorted();
162                        final int nExhibits = allExhibitNamesSorted.size();
163                        _cache_getExhibitName = allExhibitNamesSorted.toArray(new ExhibitFull[nExhibits]);
164                        cachedBasicMetaData = new GalleryBasicMetaData(aeid.timestamp, nExhibits, 1);
165                        }
166                    catch(final IOException e)
167                        { e.printStackTrace(); }
168                    }
169                }
170            }
171    
172        /**If true then we can try the local filesystem as a fallback if not in JWS.
173         * This can be an aid to development, especially if working off-line.
174         */
175        private static final boolean ALLOW_FALLBACK_FS_FILE_ACCESS = false && IsDebug.isDebug;
176    
177    
178        /**Reference to central logger; never null. */
179        private final SimpleLoggerIF logger;
180    
181    
182        /**Handle on JWS basic service; null if none.
183         * Package-visible so as to be directly usable by GUI classes.
184         */
185        final BasicService bs;
186    
187        /**Handle on JWS persistence service; null if none.
188         * Package-visible so as to be directly usable by GUI classes.
189         */
190        final PersistenceService ps;
191    
192    //    /**Handle on JWS file-open service; null if none.
193    //     * Package-visible so as to be directly usable by GUI classes.
194    //     */
195    //    final FileOpenService fos;
196    
197        /**Handle on JWS singleton service; null if none.
198         * Package-visible so as to be directly usable by GUI classes.
199         */
200        final SingleInstanceService sis;
201    
202    //    /**Handle on JWS extended service; null if none.
203    //     * Package-visible so as to be directly usable by GUI classes.
204    //     */
205    //    final ExtendedService exs;
206    
207    
208        /**Last time the user was active in this run, eg in the UI; initially object construction time.
209         * Volatile for thread-safe access without a lock.
210         */
211        private volatile long userLastActive = System.currentTimeMillis();
212    
213        /**Get the last time the user was active in this run, eg in the UI; initially zero.
214         * No harm in letting everyone see this, so is public.
215         * <p>
216         * When the user has not been active for a long time,
217         * some activities, such as polling the server, may halt or slow down
218         * to conserve resources.
219         */
220        public long getUserLastActive() { return(userLastActive); }
221    
222        /**Note user as active.
223         * Made package-visible (ie not public) for security.
224         * <p>
225         * This may be triggered/called by a number of events
226         * that indicate that the user is active.
227         * <p>
228         * When the user has not been active for a long time,
229         * some activities, such as polling the server, may halt or slow down
230         * to conserve resources.
231         */
232        void setUserLastActive(final String doingWhat)
233            {
234            userLastActive = System.currentTimeMillis();
235    //logger.log("UI active: " + doingWhat + ": " + (new Date())); }
236            }
237    
238        /**Returns true iff the user is considered inactive.
239         * This is used to reduce load on the server (and the user's machine)
240         * if they have not used the interace for circa several minutes.
241         */
242        public boolean userInactive()
243            {
244            return(System.currentTimeMillis() - userLastActive > 10 * 60 * 1000); // 10 minute limit.
245            }
246    
247        /**Cached copy of the basic Gallery meta-data; never null.
248         * Initially EMPTY.
249         * <p>
250         * Is volatile for thread-safe lock-free access.
251         */
252        private volatile LightweightMetaDataFetchInterface.GalleryBasicMetaData cachedBasicMetaData = GalleryBasicMetaData.EMPTY;
253    
254        /**Time after which we should check metadata with server.
255         * Is volatile for thread-safe lock-free access.
256         */
257        private volatile transient long _metaDataCheckTime;
258    
259        /**Return cached copy (or EMPTY if none); never null.
260         * This value is always returned from cache,
261         * ie we never block or go over the Net to get it,
262         * so this call is always fast.
263         */
264        public GalleryBasicMetaData getGalleryBasicMetaData()
265            {
266            return(cachedBasicMetaData);
267            }
268    
269        /**Cache for getExhibitName(); never null.
270         * Mapping from index to name.
271         * <p>
272         * Length should be exhibit count.
273         * <p>
274         * This may be cleared (replaced by a new empty list of the right length)
275         * when a new exhibit set is detected by a changed hash code.
276         * <p>
277         * The data itself behind the reference is logically immutable.
278         * <p>
279         * Is volatile for safe lock-free access.
280         */
281        private volatile Name.ExhibitFull[] _cache_getExhibitName = new Name.ExhibitFull[0];
282    
283        /**Set of requested exhibit names by ordinal; never null.
284         * A lock must be held on this instance during any access to it,
285         * since BitSet is not inherently thread-safe.
286         * <p>
287         * We use our poller thread to fetch names marked as needed.
288         */
289        private final BitSet _namesNeeded = new BitSet();
290    
291        /**Fetch the full name of the given numbered exhibit; null if no such exhibit or not currently available.
292         * The range is zero to numberOfExhibits-1.
293         * <p>
294         * This assumes that there is a sensible stable ordering of exhibits,
295         * probably in "smart-sorted" order or similar.
296         * <p>
297         * This ordering is fixed from one exhibit set to the next,
298         * ie is stable while the exhibit set hash remains unchanged.
299         * <p>
300         * This may go across the Net to fetch its values, so may block for a while.
301         * (We aim to cache responses, however,
302         * so that repeated retrieval of the same value should be quick,
303         * at least until the exhibit set changes.)
304         */
305        public ExhibitFull getExhibitName(final int ordinal)
306            {
307            // Capture a snapshot of the cache reference.
308            final Name.ExhibitFull[] names = _cache_getExhibitName;
309    
310            // Return null if input is out of range.
311            if((ordinal < 0) || (ordinal >= names.length)) { return(null); }
312    
313            // If the value is cached then return it immediately...
314            final Name.ExhibitFull cached = names[ordinal];
315            if(cached != null) { return(cached); }
316    
317            // Queue this name to be fetched asynchronously.
318            // Signal any worker thread that a new item is waiting...
319            synchronized(_namesNeeded)
320                {
321                _namesNeeded.set(ordinal);
322                _namesNeeded.notifyAll(); // Signal any waiting worker(s)...
323                }
324    
325            // Don't have the name ready yet.
326            return(null);
327            }
328    
329        /** Fetch the full name of the given numbered exhibits; each entry null if no such exhibit or not currently available, overall result never null.
330         * The range for each item is 0 to numberOfExhibits-1.
331         * <p>
332         * This assumes that there is a sensible stable ordering of exhibits,
333         * probably in "smart-sorted" order or similar.
334         * <p>
335         * This ordering is fixed from one exhibit set to the next,
336         * ie is stable while the exhibit set hash remains unchanged.
337         */
338        public Name.ExhibitFull[] getExhibitNames(final int ordinals[])
339            {
340            if((ordinals == null) || (ordinals.length > MAX_NAMES_REQUEST))
341                { throw new IllegalArgumentException(); }
342    
343            // Potentially rather inefficient implementation.
344            final Name.ExhibitFull result[] = new Name.ExhibitFull[ordinals.length];
345            for(int i = ordinals.length; --i >= 0; )
346                { result[i] = getExhibitName(ordinals[i]); }
347            return(result);
348            }
349    
350        /**Thumbnail (standard) rendered image width and height, positive power-of-two; fixed maximum value for all thumbnails. */
351        static final int TN_STD_IMAGE_DIM = ExhibitThumbnails.STD_STATIC_IMAGE_TN_LDIM_PX;
352    
353        /**Thumbnail (small) rendered image width and height, positive power-of-two smaller than TN_STD_IMAGE_DIM. */
354        static final int TN_SML_IMAGE_DIM = ExhibitThumbnails.SML_STATIC_IMAGE_TN_LDIM_PX;
355    
356        /**Thumbnail image format (from Java3D point of view). */
357        static final int TN_IMAGE_FORMAT = javax.media.j3d.ImageComponent.FORMAT_RGB;
358    
359        /**Power-of-two edge size of default/absent thumbnail stand-in. */
360        private static final int defaultTNImage_Edge = 16;
361    
362        /**Default thumbnail place-holder image; never null. */
363        private static final BufferedImage defaultTNImage = new BufferedImage(defaultTNImage_Edge, defaultTNImage_Edge, BufferedImage.TYPE_INT_RGB);
364    
365        /**Initialise defaultTNImage. */
366        static
367            {
368            // Solid grey.
369            for(int i = defaultTNImage_Edge; --i >= 0; )
370                {
371                for(int j = defaultTNImage_Edge; --j >= 0; )
372                    {
373                    defaultTNImage.setRGB(i, j, 0xFFCCCCCC);
374                    }
375                }
376    
377            // Red diagonal cross to indicate nothing present...
378            for(int i = defaultTNImage_Edge; --i >= 0; )
379                {
380                defaultTNImage.setRGB(i, i, 0xFFFF0000);
381                defaultTNImage.setRGB(i, defaultTNImage_Edge - 1 - i, 0xFFFF0000);
382                }
383            }
384    
385        /**Default flags for loading textures.
386         * Using BY_REFERENCE may reduce memory usage
387         * (and make the texture cache work better)
388         * at the cost of some rendering performance.
389         */
390        private static final int TEXTURE_LOADER_FLAGS = TextureLoader.BY_REFERENCE;
391    
392        /**Default thumbnail place-holder image as a texture; never null. */
393        private static final Texture defaultTNTexture = (new TextureLoader(defaultTNImage, TextureLoader.BY_REFERENCE)).getTexture();
394    
395        /**Thumbnail image for items not yet loaded, minimal power-of-two-edge size; never null. */
396        private static final BufferedImage notLoadedTNImage = new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB);
397    
398        /**Initialise notLoadedTNImage. */
399        static
400            {
401            // Solid light red (1 pixel).
402            notLoadedTNImage.setRGB(0, 0, 0xFFFF3333);
403            }
404    
405        /**Thumbnail not-loaded image as a texture; never null. */
406        private static final Texture notLoadedTNTexture = (new TextureLoader(notLoadedTNImage, TextureLoader.BY_REFERENCE)).getTexture();
407    
408    
409        /**Maximum number of small thumbnail binaries to cache in memory to limit resource use; strictly positive.
410         * Each small thumbnail image binary may take up to about 4kBytes.
411         * <p>
412         * We cap this to a fraction (~20%) of estimated total JVM memory available,
413         * though ensure that a we always set a minimum useful size.
414         * (If maxMemory() is not limited/set then we use totalMemory().)
415         */
416        private final static int MAX_SML_TN_CACHE_IN_MEMORY = Math.max(4001,
417            (int) (((Runtime.getRuntime().maxMemory() != Long.MAX_VALUE) ? Runtime.getRuntime().maxMemory() : Runtime.getRuntime().totalMemory()) /
418                (5 * ExhibitThumbnails.SML_ABS_MAX_BYTES)));
419    //    static { if(IsDebug.isDebug) { System.out.println("MAX_SML_TN_CACHE_IN_MEMORY="+MAX_SML_TN_CACHE_IN_MEMORY); } }
420    
421        /**LRU cache of small thumbnail images for getThumbnailImageAsTexture() in binary form; never null.
422         * We hold the binary (compressed) format of the thumbnails,
423         * hoping that this does not exhaust memory,
424         * so that we can more-or-less guarantee to show something quickly
425         * once it has been loaded.
426         * <p>
427         * A zero-length entry indicates a permanent failure to load the thumbnail.
428         */
429        private final SimpleLRUMap<Name.ExhibitFull, ROByteArray> _cache_getThumbnailImage_sml_binary = SimpleLRUMap.create(MAX_SML_TN_CACHE_IN_MEMORY, "_cache_getThumbnailImage_sml_binary");
430    
431        /**Cache of small thumbnail images for getThumbnailImageAsTexture(); never null.
432         * Mapping from full exhibit name to SoftReference to the standard (larger)
433         * thumbnail/texture.
434         * <p>
435         * We use a non-strong reference because the memory requirements may be
436         * large and unpredictable.
437         * <p>
438         * TODO: This should be purged of stale entries periodically
439         * and/or when a new exhibit set is detected by a changed hash code
440         * in case those entries refer to exhibits that no longer exist.
441         * <p>
442         * Thread-safe.
443         */
444        private final Map<Name.ExhibitFull, SoftReference<Texture>> _cache_getThumbnailImage_sml = new Hashtable<Name.ExhibitFull, SoftReference<Texture>>();
445    
446        /**Cache of standard (larger) thumbnail images for getThumbnailImageAsTexture(); never null.
447         * Mapping from full exhibit name to SoftReference to the standard (larger)
448         * thumbnail/texture.
449         * <p>
450         * We use a non-strong reference because the memory requirements may be
451         * large and unpredictable.
452         * <p>
453         * TODO: This should be purged of stale entries periodically
454         * and/or when a new exhibit set is detected by a changed hash code
455         * in case those entries refer to exhibits that no longer exist.
456         * <p>
457         * Thread-safe.
458         */
459        private final Map<Name.ExhibitFull, SoftReference<Texture>> _cache_getThumbnailImage_std = new Hashtable<Name.ExhibitFull, SoftReference<Texture>>();
460    
461        /**Maximum number of thumbnails to fetch in parallel; strictly positive.
462         * Too large a number may overwhelm (or be rejected by) the server.
463         * <p>
464         * Set to more than the number of available processors,
465         * to help make use of available resources and overcome I/O latency,
466         * though capped to protect our resources and those of the server.
467         */
468        private static final int MAX_TN_CONC_FETCHES = Math.min(5, 1 + Runtime.getRuntime().availableProcessors());
469    
470        /**Map of names of exhibits whose thumbnails currently being fetched to the Threads fetching them; never null.
471         * Thread-safe.
472         * <p>
473         * This may be shared with other per-exhibit threads.
474         * <p>
475         * The size of this should never be larger than MAX_TN_CONC_FETCHES.
476         */
477        private final Map<Name.ExhibitFull, Thread> thumbnailsBeingFetched = new Hashtable<Name.ExhibitFull, Thread>();
478    
479        /**Minimum time in milliseconds before retrying a thumbnail fetch after any previous failure to fetch it; strictly positive. */
480        private static final int MIN_TN_RETRY_MS = 60000; // 1 minute.
481    
482        /**Map of thumbnail's full exhibit name to time before we may attempt to refetch following a failure; never null.
483         * Thread-safe.
484         */
485        private final Map<Name.ExhibitFull, Long> tnNoRetryBefore = new Hashtable<Name.ExhibitFull, Long>();
486    
487        /**Get thumbnail image as a power-of-two-side square Texture for given image index.
488         * Returns a logically-read-only RGB-format image for the given exhibit
489         * in a fixed-size (maximum dimension for a standard thumbnail) rect.
490         * <p>
491         * This routine assumes that the caller will never alter the result,
492         * so instances can be safely cached/shared.
493         * <p>
494         * If the exhibit does not exist or a thumbnail is not available
495         * then this returns a standard "grey"/not-present image.
496         * <p>
497         * An out-of-range request returns a different "not-loaded" image.
498         * <p>
499         * The underlying image is (re)fetched from the server and cached as need be
500         * which should be largely invisible to the Java3D rendering engine.
501         * <p>
502         * This may release image data that is not used for a long time.
503         * <p>
504         * This fetches low-priority requests only when not fetching any others.
505         * <p>
506         * Kept package-visible-only for now.
507         *
508         * @param ordinal  index of thumbnail texture to retrieve,
509         *     else -1 to get standard "not-yet-fetched" thumbnail texture
510         * @param std  if true then fetch standard thumbnail, else small
511         * @param lowPriority  fetch the texture only if the system is quiet
512         *
513         * @return null if currently fetching image or too busy to do so,
514         *     else the thumbnail as a texture if available
515         *     else a standard "no-image" texture
516         */
517        Texture getThumbnailImageAsTexture(final int ordinal,
518                                           final boolean std,
519                                           final boolean lowPriority)
520            {
521            // For negative (always invalid) request return "not-loaded" texture.
522            if(ordinal < 0)
523                { return(notLoadedTNTexture); /* Should be read-only or a copy... */ }
524    
525            // If user is not currently active
526            // then refuse to fetch thumbnails to save load here and on the server.
527            if(userInactive()) { return(null); }
528    
529            // Return null texture if we cannot get the exhibit's name.
530            // This may be because the name is not yet in cache
531            // and we do not want to block...
532            final Name.ExhibitFull exhibitFullName = getExhibitName(ordinal);
533    //        if(!ExhibitName.validNameSyntax(exhibitName))
534    //            { return(null); /* Should be read-only or a copy... */ }
535    
536            // If any other fetch threads are running
537            // then refuse to handle a low-priority request right now.
538            // We don't even need to lookup the exhibit name, etc.
539            // (We were prepared, however, to get the name fetched above.)
540            if(lowPriority && !thumbnailsBeingFetched.isEmpty())
541                {
542    //if(IsDebug.isDebug) { logger.log("[Deferring low-priority thumbnail fetch for #"+ordinal+".]"); }
543                return(null);
544                }
545    
546            // Get a handle on the appropriate thumbnail texture cache...
547            final Map<Name.ExhibitFull, SoftReference<Texture>> cache =
548                std ? _cache_getThumbnailImage_std : _cache_getThumbnailImage_sml;
549    
550            // If we have something already cached then return it immediately...
551            final SoftReference<Texture> sr = cache.get(exhibitFullName);
552            if(sr != null)
553                {
554                final Texture texture = sr.get();
555                if(texture != null) { return(texture); }
556                }
557    
558            // Return default texture for unsupported type
559            // (ie if not a still-image type).
560            final ExhibitMIME.ExhibitTypeParameters exhibitType = ExhibitMIME.getInputFileType(exhibitFullName);
561            if(!Utils.supportedTypeFor3DWT(exhibitType))
562                { return(defaultTNTexture); /* Should be read-only or a copy... */ }
563    
564            // Check if we have a block on fetching this thumbnail.
565            final Long l = tnNoRetryBefore.get(exhibitFullName);
566            if((l != null) && (l.longValue() > System.currentTimeMillis()))
567                { return(null); }
568    
569            // Avoid races when starting new threads...
570            synchronized(thumbnailsBeingFetched)
571                {
572                // If this is already in the process of being fetched,
573                // or we are already concurrently fetching as many as is allowed,
574                // then return null now.
575                Thread th = thumbnailsBeingFetched.get(exhibitFullName);
576                if((th != null) && !th.isAlive())
577                    {
578                    thumbnailsBeingFetched.remove(exhibitFullName);
579                    logger.log("WARNING: removed dead thread for fetching " + exhibitFullName);
580                    th = null;
581                    }
582                // Give hi-res/high-priority fetches priority:
583                // don't let lo-res/low-priority fetches take the last slot/thread.
584                final int inProgress = thumbnailsBeingFetched.size();
585                if((th != null) ||
586                   (inProgress >= MAX_TN_CONC_FETCHES) ||
587                   (lowPriority && !std && (inProgress > 0) && (inProgress >= MAX_TN_CONC_FETCHES-1)))
588                    { return(null); }
589    
590                // Just show the short name to reduce "noise" for the user...
591                final Name.ExhibitShort shortName = exhibitFullName.getShortName();
592    
593                // Create new thread to fetch texture not in the cache...
594                // We also regulate system load (client and server) a little here.
595                final Thread fetchThread = new Thread("fetching thumbnail for " + shortName){
596                    @Override public final void run()
597                        {
598                        final long startTime = System.currentTimeMillis();
599    
600                        try
601                            {
602                            // Put a block on retrying any refetch after this...
603                            // And avoid any concurrent/duplicate fetch.
604                            tnNoRetryBefore.put(exhibitFullName, System.currentTimeMillis() + MIN_TN_RETRY_MS);
605    
606                            // Note fetch/refetch in status area...
607                            logger.log(((cache.get(exhibitFullName) != null) ? "Refetching" : "Fetching")+
608                                       " thumbnail for "+shortName+"...");
609    
610                            // Clear any stale value from the cache.
611                            cache.remove(exhibitFullName);
612    
613                            // Attempt to fetch the texture synchronously...
614                            fetchTexture(exhibitFullName, cache, std);
615    
616                            if(cache.get(exhibitFullName) != null)
617                                {
618                                // Assumed to be a success
619                                // (or a permanent failure).
620                                // Allow an immediate refetch in future if need be.
621                                tnNoRetryBefore.remove(exhibitFullName);
622                                }
623                            else
624                                {
625                                // Assume that we failed: postpone any retry.
626                                // Add in random component to help avoid collisions.
627                                tnNoRetryBefore.put(exhibitFullName, System.currentTimeMillis() + MIN_TN_RETRY_MS +
628                                                          Rnd.fastRnd.nextInt(MIN_TN_RETRY_MS));
629                                }
630                            }
631                        finally
632                            {
633                            if(thumbnailsBeingFetched.size() == 1)
634                                { logger.log("Finished fetching thumbnail for "+shortName+"."); }
635    
636                            // Regulate load, at least for low-priority fetches.
637                            if(lowPriority)
638                                {
639                                try
640                                    {
641                                    // Try to make sure that we do not consume all CPU
642                                    // on this client system nor at the server
643                                    // by sleeping a multiple of fetch wall-clock time.
644                                    // Put an upper bound of a few seconds on this.
645                                    Thread.sleep(1 + Math.min(10101, Math.max(1,
646                                        2 * (System.currentTimeMillis() - startTime))));
647                                    }
648                                catch(final InterruptedException e)
649                                    {
650                                    e.printStackTrace(); /* Absorb error, though whinge and finish immediately... */
651                                    }
652                                }
653    
654                            assert(this == thumbnailsBeingFetched.get(exhibitFullName));
655                            thumbnailsBeingFetched.remove(exhibitFullName);
656    
657                            if(thumbnailsBeingFetched.isEmpty())
658                                { logger.log("Idle."); }
659                            }
660                        }
661                    };
662                fetchThread.setDaemon(true);
663                // Attempt to actually run low-priority fetches
664                // on lower-than-normal (lower-than-caller) priority threads...
665                fetchThread.setPriority(Math.max(Thread.currentThread().getPriority()-(lowPriority?4:2), Thread.MIN_PRIORITY));
666                thumbnailsBeingFetched.put(exhibitFullName, fetchThread);
667                fetchThread.start();
668                }
669    
670            // Do not wait for result...
671            return(null); // In process of fetching texture...
672            }
673    
674        /**Root-relatve absolute URL prefix for standard thumbnails; starts and ends with '/'. */
675        private static final String TN_STD_RRURL_PREFIX = "/_tn/std/";
676    
677        /**Root-relatve absolute URL prefix for small thumbnails; starts and ends with '/'. */
678        private static final String TN_SML_RRURL_PREFIX = "/_tn/sml/";
679    
680        /**If true then allow fetchTexture() to return a small thumbnail where the standard one is unavailable. */
681        private static final boolean ALLOW_TEXTURE_FALLBACK = true;
682    
683        /**If true then allow local persistance of small thumbnails in their binary form using JWS.
684         * As of 200603 there seems to be a problem storing more than 255 muffins total!
685         */
686        private static final boolean CACHE_JWS_TN_SML = false;
687    
688        /**If true then allow local cacheing of small thumbnails in their binary form in memory.
689         */
690        private static final boolean CACHE_MEM_TN_SML = true;
691    
692        /**Adjust URLs to be suitable for JWS muffins.
693         * JWS does not seem to like directory components,
694         * so we flatten the URL at some slight risk of ambiguity.
695         */
696        private static URL _adjustURLForMuffin(final URL full)
697            throws MalformedURLException
698            {
699            return(new URL(full.getProtocol(),
700                           full.getHost(),
701                           full.getPort(),
702    //                       (full.getFile().startsWith("/") ? full.getFile().substring(1) : full.getFile())));
703                           full.getFile().replace('/', '-')));
704            }
705    
706        /**Private cache value for fetchTexture(). */
707        private transient volatile Object _c_fT;
708    
709        /**Fetch the image/texture synchronously from the server.
710         * Caches the image if successful,
711         * else leaves the cache slot alone.
712         * <p>
713         * This never deletes cache entries.
714         * <p>
715         * This may try to fall back to the small thumbnail
716         * if the large one cannot be fetched...
717         *
718         * @param exhibitName  the full valid name of the exhibit
719         * @param cache  the cache to save the texture in
720         * @param std  if true then fetch standard thumbnail, else small
721         */
722        private void fetchTexture(final Name.ExhibitFull exhibitName,
723                                  final Map<Name.ExhibitFull, SoftReference<Texture>> cache,
724                                  final boolean std)
725            {
726    //        assert(ExhibitName.validNameSyntax(exhibitName));
727            assert(cache != null);
728    
729    if(IsDebug.isDebug) { System.out.println("Request for thumbnail: " + ExhibitName.getFileComponent(exhibitName)); }
730    
731            // Note if raw thumbnails image binary read from local cache;
732            // if so, don't write it back to the same cache!
733            boolean readFromMemory = false;
734            boolean readFromPersistentStore = false;
735    
736            // Get thumbnail as (encoded) input stream...
737            InputStream is = null;
738            try
739                {
740                // Try to fetch thumbnail from memory if possible.
741                if(CACHE_MEM_TN_SML && !std)
742                    {
743                    final ROByteArray binaryData = _cache_getThumbnailImage_sml_binary.get(exhibitName);
744                    if((binaryData != null) && (binaryData.length() > 0))
745                        {
746                        is = new ByteArrayInputStream(binaryData.toByteArray());
747                        readFromMemory = true;
748    if(IsDebug.isDebug) { System.out.println("Retrieved thumbnail from memory store: " + exhibitName); }
749                        }
750                    }
751    
752                // Try to fetch image relative to code base by preference.
753                // Construct URL to fetch thumbnail.
754    //        final String tnRRL = WebUtils.makeThumbnailRRURL(exhibitName, true);
755                final String tnRRL = (std ? TN_STD_RRURL_PREFIX : TN_SML_RRURL_PREFIX)+ exhibitName;
756                URL tnURL = null;
757                if(bs != null)
758                    {
759                    try { tnURL = new URL(bs.getCodeBase(), tnRRL); }
760                    catch(final Exception e) { if(IsDebug.isDebug) { e.printStackTrace(); } }
761                    }
762    
763                // Allow read from local filesystem exhibit store if enabled...
764                // (And if we haven't already got the data we need...)
765                if(ALLOW_FALLBACK_FS_FILE_ACCESS && (tnURL == null) && (is == null))
766                    {
767                    try
768                        {
769                        // Create file source on first use...
770                        ExhibitDataFileSource edfs = (ExhibitDataFileSource) _c_fT;
771                        if(edfs == null) { _c_fT = edfs = new ExhibitDataFileSource(logger); }
772                        assert(edfs != null);
773                        final AllExhibitProperties aep = edfs.getAllExhibitProperties(-1);
774                        final ExhibitStaticAttr esa = aep.aeid.getStaticAttr(exhibitName);
775                        if(esa == null)
776                            {
777                            System.out.println("fetchTexture(): cannot find local exhibit: " + exhibitName);
778                            cache.put(exhibitName, new SoftReference<Texture>(defaultTNTexture));
779                            return;
780                            }
781                        else
782                            {
783                            // OK, attempt to generate thumbnails here, not under any lock.
784                            final ExhibitMIME.ExhibitTypeParameters exhibitType = (ExhibitMIME.getInputFileType(esa.getCharSequence()));
785                            final ExhibitThumbnails result = exhibitType.handler.makeThumbnails(
786                                        esa,
787                                        edfs.makeExhibitDataSource(),
788                                        aep,
789                                        true);
790    
791                            if(result == null)
792                                { return; /* Not available yet; don't update cache. */ }
793                            final ExhibitThumbnails.Thumbnail thumbnail;
794                            if((thumbnail = (std ? result.getStandard() : result.getSmall())) != null)
795                                {
796                                System.out.println("fetchTexture(): generated thumbnail for local exhibit: " + exhibitName);
797                                is = new ByteArrayInputStream(thumbnail.toByteArrray());
798                                }
799                            else if(ALLOW_TEXTURE_FALLBACK && (std == true))
800                                {
801                                fetchTexture(exhibitName, cache, false);
802                                return;
803                                }
804                            else
805                                {
806    if(IsDebug.isDebug) { System.out.println("Request for thumbnail failed from local filesystem."); }
807                                cache.put(exhibitName, new SoftReference<Texture>(defaultTNTexture));
808                                return;
809                                }
810                            }
811                        }
812                    catch(final Throwable t)
813                        {
814                        t.printStackTrace();
815                        // If we fail to create from local filesystem
816                        // then don't try again...
817    if(IsDebug.isDebug) { System.out.println("Request for thumbnail failed from local filesystem with an IOException."); }
818                        cache.put(exhibitName, new SoftReference<Texture>(defaultTNTexture));
819                        return;
820                        }
821                    }
822    
823                // Try to fetch tn URL as a stream from local persistent store.
824                // Try this for all URLs as it should be quick...
825                if((CACHE_JWS_TN_SML) && (ps != null) && (is == null) && (tnURL != null))
826                    {
827                    try
828                        {
829                        final FileContents fileContents = ps.get(_adjustURLForMuffin(tnURL));
830                        is = fileContents.getInputStream();
831                        readFromPersistentStore = true;
832    if(IsDebug.isDebug) { System.out.println("Read thumbnail from persistent store: " + tnURL); }
833                        }
834                    catch(final FileNotFoundException e) { /* e.printStackTrace(); */ }
835                    catch(final Throwable t) { t.printStackTrace(); }
836                    }
837    
838                // Fallback...
839                // If JWS codebase is not available then try the main data host.
840                if((tnURL == null) && (is == null))
841                    {
842                    try { tnURL = new URL("http", CoreConsts.MAIN_DATA_HOST, tnRRL); }
843                    catch(final Exception e) { if(IsDebug.isDebug) { e.printStackTrace(); } }
844                    }
845    
846                // Try to fetch tn URL as a stream...
847                if((is == null) && (tnURL != null))
848                    {
849    if(IsDebug.isDebug) { System.out.println("Will try to fetch thumbnail from: " + tnURL); }
850                    final URLConnection urlConnection = tnURL.openConnection();
851                    // Adjust the connection properties to still behave OK
852                    // given a slow/lossy connection.
853                    urlConnection.setConnectTimeout(Utils.RPC_HTTP_CONNECT_TIMEOUT_MS); // Cap the connect time in case of slow/lossy network.
854                    urlConnection.setReadTimeout(Utils.RPC_HTTP_READ_TIMEOUT_MS); // Avoid getting stuck in a read too.
855                    urlConnection.setUseCaches(true); // Take advantage of any cacheing available!
856                    urlConnection.setAllowUserInteraction(false);
857                    urlConnection.setRequestProperty("Referer", "http://" + CoreConsts.MAIN_DATA_HOST + Utils.SERVLET_MOUNT_POINT);
858                    if(urlConnection instanceof HttpURLConnection)
859                        {
860                        final HttpURLConnection huc = (HttpURLConnection) urlConnection;
861                        final int responseCode = huc.getResponseCode();
862                        final int topDigit = responseCode / 100;
863                        if(topDigit == 4)
864                            {
865                            // For a permanent failure use the default texture.
866                            // We should not try again.
867                            // Note that the strong reference to the default texture
868                            // should keep this SoftReference live indefinitely.
869    if(IsDebug.isDebug) { System.out.println("Request for thumbnail failed with code "+responseCode+"."); }
870    
871                            if(ALLOW_TEXTURE_FALLBACK && (std == true))
872                                { fetchTexture(exhibitName, cache, false); }
873                            else
874                                { cache.put(exhibitName, new SoftReference<Texture>(defaultTNTexture)); }
875                            return;
876                            }
877                        }
878                    is = urlConnection.getInputStream();
879                    }
880    
881                // If we have the input stream, then try to:
882                //   * copy/cache the data if applicable
883                //   * convert to a texture (and cache the texture)!
884                if(is != null)
885                    {
886                    // If we are allowed to persist locally
887                    // then keep a copy of the input stream.
888                    // Don't (re)cache in persistent store if fetched from memory.
889                    final boolean canCacheInMemory = (CACHE_MEM_TN_SML && !std) && !readFromMemory;
890                    final boolean canPersistData = !readFromMemory && (CACHE_JWS_TN_SML && !std) && !readFromPersistentStore && (ps != null);
891                    final boolean captureDataCopy = canPersistData || canCacheInMemory;
892                    if(captureDataCopy)
893                        {
894                        final int maxSize = std ? ExhibitThumbnails.STD_ABS_MAX_BYTES : ExhibitThumbnails.SML_ABS_MAX_BYTES;
895    
896                        final InputStream oldIs = is;
897    
898                        final ByteArrayOutputStream dataCopy = new ByteArrayOutputStream(maxSize);
899                        final byte buf[] = new byte[1024];
900                        for( ; ; )
901                            {
902                            final int n = is.read(buf);
903                            if(n < 1) { break; /* EOF */ }
904                            dataCopy.write(buf, 0, n);
905    
906                            if(dataCopy.size() > maxSize)
907                                { throw new IOException("corrupt thumbnail data (too large): "+dataCopy.size()+" vs "+maxSize); }
908                            }
909    
910                        // Close the original input stream...
911                        oldIs.close();
912    
913                        // Replace with new buffered input.
914                        is = (new ByteArrayInputStream(dataCopy.toByteArray()));
915    
916                        // Persist the tn data that we have just collected...
917                        // Give up quietly if this fails for any reason
918                        // (eg it already exists, because we just read from it).
919                        try
920                            {
921                            if(canCacheInMemory)
922                                {
923                                // Cache in memory...
924                                _cache_getThumbnailImage_sml_binary.put(exhibitName, new ROByteArray(dataCopy.toByteArray()));
925    if(IsDebug.isDebug) { System.out.println("Wrote thumbnail to memory store: " + exhibitName); }
926                                }
927                            if(canPersistData)
928                                {
929                                final URL flattenedURL = _adjustURLForMuffin(tnURL);
930                                if(ps.create(flattenedURL, dataCopy.size()) >= dataCopy.size())
931                                    {
932                                    // Mark file as cached copy, ie discardable at will...
933                                    ps.setTag(flattenedURL, PersistenceService.CACHED);
934                                    final FileContents fileContents = ps.get(flattenedURL);
935                                    final OutputStream os = fileContents.getOutputStream(true); // Replace any extant entry...
936                                    os.write(dataCopy.toByteArray());
937                                    os.close();
938    if(IsDebug.isDebug) { System.out.println("Wrote thumbnail to persistent store: " + tnURL); }
939                                    }
940                                else { ps.delete(flattenedURL); } // Not enough space, so zap muffin.
941                                }
942                            }
943                        catch(final Throwable t) { t.printStackTrace(); /* Whinge but ignore. */ }
944                        }
945    
946                    try
947                        {
948                        // Synchronous thumbnail image loading...
949    //                    final long imgPrepStart = System.currentTimeMillis();
950                        final BufferedImage bufferedImageRawTN = javax.imageio.ImageIO.read(is);
951                        if(bufferedImageRawTN == null)
952                            { throw new IOException("could not decode image"); }
953                        final BufferedImage bufferedImageTrueCol = ImageUtils.convertToTrueColourARGB(bufferedImageRawTN, false);
954                        final int height = bufferedImageTrueCol.getHeight();
955                        final int width = bufferedImageTrueCol.getWidth();
956                        final int targetDim = std ? TN_STD_IMAGE_DIM : TN_SML_IMAGE_DIM;
957                        final BufferedImage thImage = new BufferedImage(targetDim, targetDim, BufferedImage.TYPE_INT_ARGB);
958                        final int bgRGB = std ? 0x80333333 : 0x80CC9999;
959                        // TODO: make this all more efficient!
960    //                    final long imgPrep0 = System.currentTimeMillis();
961                        for(int x = targetDim; --x >= 0; )
962                            {
963                            for(int y = targetDim; --y >= 0; )
964                                {
965                                // Possible partial transparency...
966                                thImage.setRGB(x, y, bgRGB);
967                                }
968                            }
969    //                    final long imgPrep1 = System.currentTimeMillis();
970                        final int left = (targetDim-width)/2;
971                        final int top = (targetDim-height)/2;
972    //                compositeImage.getSubimage(left, top, width, height).getRaster().
973    //                    setRect(bufferedImageTrueCol.getRaster());
974                        for(int x = width; --x >= 0; )
975                            {
976                            final int xo = x + left;
977                            if((xo < 0) || (xo >= targetDim)) { continue; }
978                            for(int y = height; --y >= 0; )
979                                {
980                                final int yo = y + top;
981                                if((yo < 0) || (yo >= targetDim)) { continue; }
982                                thImage.setRGB(xo, yo, bufferedImageTrueCol.getRGB(x, y));
983                                }
984                            }
985    //                    final long imgPrep2 = System.currentTimeMillis();
986                        final TextureLoader textureLoader = new TextureLoader(thImage, TEXTURE_LOADER_FLAGS);
987                        final Texture texture = textureLoader.getTexture();
988    //                    final long imgPrep3 = System.currentTimeMillis();
989    //if(IsDebug.isDebug)
990    //    {
991    //    System.out.println("  imgPrep "+(std?"std":"sml")+" (ms) 0/1/2/3: " + (imgPrep0-imgPrepStart) + "/" + (imgPrep1-imgPrep0)+ "/" + (imgPrep2-imgPrep1) + "/" + (imgPrep3-imgPrep2));
992    //    }
993                        if(texture != null) // Success!
994                            {
995    if(IsDebug.isDebug) { System.out.println("Request for thumbnail succeeded: " + exhibitName); }
996                            cache.put(exhibitName, new SoftReference<Texture>(texture));
997                            return; // Done!
998                            }
999                        }
1000                    // Possibly want to defer any further attempt
1001                    // to fetch this thumbnail for a while...
1002                    catch(final Throwable t) { t.printStackTrace(); }
1003                    }
1004                }
1005            catch(final IOException e)
1006                {
1007    //            e.printStackTrace(); // Failed...
1008                logger.log("[Problem fetching thumbnail "+exhibitName+": " + e.getMessage());
1009                }
1010            finally
1011                {
1012                // Release resources ASAP.
1013                if(is != null)
1014                    {
1015                    try { is.close(); /* Close any open stream... */ }
1016                    catch(final IOException e) { e.printStackTrace(); }
1017                    }
1018                }
1019    
1020            // Failed; leave cache untouched.
1021    if(IsDebug.isDebug) { System.out.println("Request for thumbnail failed: " + exhibitName); }
1022            }
1023    
1024    
1025        /**Load any (large) persisted data from a previous execution and start a background worker thread.
1026         * Do this as early as possible after construction,
1027         * and preferably before allowing (much) user interaction.
1028         * <p>
1029         * We spin this off into a separate thread to avoid blocking startup.
1030         * <p>
1031         * This should be called at most once.
1032         * <p>
1033         * Package-visible so as to be directly usable by GUI classes.
1034         */
1035        void startup()
1036            {
1037            final Thread th = new Thread("3D logic startup()"){
1038                /**Do load work as background thread. */
1039                @Override
1040                public final void run()
1041                    {
1042    //                if((ps != null) && (bs != null))
1043    //                    {
1044    //                    // ...
1045    //                    }
1046    
1047                    // Run the first poll manually/synchronously.
1048                    poll();
1049    
1050                    // Once we have finished reloading any persisted data,
1051                    // start a (daemon) poller thread for non-UI async activity
1052                    // such as fetching any updated AEP over the tunnel.
1053                    // After a short delay, run approximately every second or so.
1054                    // This is created/started after initial UI object construction
1055                    // is complete to avoid any unpleasant races.
1056                    final Timer timer = new Timer(true);
1057                    timer.schedule(new TimerTask(){
1058                        /**The action to be performed by this task. */
1059                        @Override
1060                        public final void run()
1061                            {
1062                            if(shuttingDown) { cancel(); }
1063                            else { poll(); }
1064                            }
1065                        }, 500, 901 + Rnd.fastRnd.nextInt(1003));
1066                    }
1067                };
1068            th.setDaemon(true); // Don't prevent the application from exiting...
1069            th.start();
1070    
1071            // Try to fetch names with high priority.
1072            try { nameFetchWorker.setPriority(Thread.MAX_PRIORITY); }
1073            catch(final Throwable t) { }
1074            nameFetchWorker.setDaemon(true); // Don't prevent the application from exiting...
1075            nameFetchWorker.start();
1076    
1077            // Wait a short while for first poll to complete before returning
1078            // in the hope that this might save doing some UI work twice...
1079            try { th.join(CoreConsts.MAX_INTERACTIVE_DELAY_MS); }
1080            catch(final InterruptedException e) { }
1081            }
1082    
1083    //    /**Make the props persistence URL; only viable when in JWS/JNLP. */
1084    //    private static URL makePropsURL(final BasicService bs) throws IOException
1085    //        {
1086    //        final URL codebase = bs.getCodeBase();
1087    //        final URL urlProps = new URL(codebase, FNAME_MAIN_PROPS);
1088    //        return(urlProps);
1089    //        }
1090    
1091    
1092        /**Called (by a daemon thread) to perform async activity.
1093         * This is not Swing-safe, and does not block Swing if blocked.
1094         * <p>
1095         * This is not called until construction is complete.
1096         * <p>
1097         * Package-visible so as to be directly usable by the main GUI class.
1098         */
1099        private void poll()
1100            {
1101            final long pollStart = System.currentTimeMillis();
1102            try
1103                {
1104                // If user is not currently active
1105                // then refuse to do the polling to save load here and on server.
1106                if(userInactive()) { return; }
1107    
1108                // Where we have picked up a local exhibit set,
1109                // don't confuse things by trying to get metadata from a server!
1110                final boolean useLocalExhibitSet =
1111                    ALLOW_FALLBACK_FS_FILE_ACCESS && (bs == null) && (cachedBasicMetaData.exhibitCount != 0);
1112    
1113                if(!useLocalExhibitSet && (pollStart > _metaDataCheckTime) &&
1114                   Rnd.fastRnd.nextBoolean() /* Sometimes skip this read attempt. */ )
1115                    {
1116                    try
1117                        {
1118    if(IsDebug.isDebug) { logger.log("[Fetching metadata from server...]"); }
1119    
1120                        // Time to check the exhibit metadata with the server,
1121                        // and clear caches as appropriate
1122                        // if the exhibit set has changed.
1123                        final GalleryBasicMetaData md =
1124                            Utils.getMetaData((bs == null) ? null : bs.getCodeBase());
1125    if(IsDebug.isDebug) { logger.log("[Fetched metadata: "+md+".]"); }
1126                        if(cachedBasicMetaData.hashCode() != md.hashCode())
1127                            {
1128    if(IsDebug.isDebug) { logger.log("[Metadata has changed: "+md+".]"); }
1129                            // Data has changed; update our caches!
1130                            _cache_getExhibitName = new Name.ExhibitFull[md.exhibitCount];
1131                            cachedBasicMetaData = md;
1132                            synchronized(_namesNeeded) { _namesNeeded.clear(); }
1133                            // TODO: start cleaner thread for texture/image cache...
1134                            }
1135                        // Put off the next metadata fetch as suggested by the server.
1136                        // Coerce the suggested wait to reasonable bounds.
1137                        _metaDataCheckTime = pollStart + Math.max(60000, // Avoid pestering server too often.
1138                            Math.min(CoreConsts.DEFAULT_TEMPORAL_SLACKNESS_S * 750,
1139                                     md.metaDataMaxCacheLifeMs)) +
1140                            // Throw in a random component to help unsync requests.
1141                            Rnd.fastRnd.nextInt(10201);
1142                        }
1143                    catch(final IOException e)
1144                        {
1145                        e.printStackTrace(); /* Absorb error but report it. */
1146                        logger.log("Problem fetching gallery metadata, please check your network connection: " + e.getMessage());
1147                        }
1148                    }
1149    
1150                // Pitch in with the dedicated worker thread for a short while
1151                // to fetch any names that have been requested (by index).
1152                final long nameFetchStart = System.currentTimeMillis();
1153                final long stopBy = nameFetchStart + 1003;
1154                _fetchNames(stopBy, _cache_getExhibitName);
1155    
1156                // Note count of names still queued for fetching...
1157                synchronized(_namesNeeded)
1158                    {
1159                    if(!_namesNeeded.isEmpty())
1160                        { logger.log("[Names still to fetch: "+_namesNeeded.cardinality()+"...]"); }
1161                    }
1162    
1163    //if(IsDebug.isDebug) { System.out.println("[Memory use free/total = "+Runtime.getRuntime().freeMemory()+"/"+Runtime.getRuntime().totalMemory()+".]"); }
1164                }
1165            catch(final Throwable t)
1166                {
1167                t.printStackTrace();  /* Absorb error, but whinge. */
1168                }
1169            }
1170    
1171        /**Worker thread that does nothing but get names in the background.
1172         * Exits when this is shut down.
1173         */
1174        private final Thread nameFetchWorker = new Thread("name fetch worker"){
1175            @Override public final void run()
1176                {
1177                while(!shuttingDown)
1178                    {
1179                    try
1180                        {
1181                        // Wait until we see a pending name request.
1182                        synchronized(_namesNeeded)
1183                            {
1184                            while(_namesNeeded.isEmpty())
1185                                { _namesNeeded.wait(1111 + Rnd.fastRnd.nextInt(1001)); }
1186                            }
1187    
1188                        // Spend a little while getting requested names.
1189                        _fetchNames(System.currentTimeMillis() + 120000, _cache_getExhibitName);
1190                        }
1191                    catch(final Throwable t)
1192                        {
1193                        t.printStackTrace();  /* Absorb error, but whinge. */
1194                        }
1195                    }
1196                }
1197            };
1198    
1199        /**Fetch all outstanding names that we can, up to the specified time limit.
1200         * It is safe to run two or more of these concurrently,
1201         * though there is a small risk of races causing duplicate fetches.
1202         * Note really efficient at high levels of concurrency.
1203         */
1204        private void _fetchNames(final long stopBy, final Name.ExhibitFull[] names)
1205            {
1206            final List<Integer> needed = new ArrayList<Integer>(LightweightMetaDataFetchInterface.MAX_NAMES_REQUEST);
1207            for(int nextNameNeeded = -1; System.currentTimeMillis() < stopBy; )
1208                {
1209                // Find next needed names index under the lock,
1210                // else quit if none needed.
1211                // Collect an efficient-size block,
1212                // and clear them as we go to prevent another thread
1213                // trying to collect the same ones.
1214                needed.clear();
1215                synchronized(_namesNeeded)
1216                    {
1217                    do
1218                        {
1219                        // Look for next name.
1220                        nextNameNeeded = _namesNeeded.nextSetBit(++nextNameNeeded);
1221                        if(nextNameNeeded < 0)
1222                            { break; /* Finished; no more names to fetch. */ }
1223                        else if(nextNameNeeded >= names.length)
1224                            {
1225                            _namesNeeded.clear(); /* Spurious values presumably after exhibit set shrank. */
1226                            break; /* Done. */
1227                            }
1228                        else if(names[nextNameNeeded] != null)
1229                            {
1230                            _namesNeeded.clear(nextNameNeeded); /* Already fetched. */
1231                            continue;
1232                            }
1233    
1234                        needed.add(Integer.valueOf(nextNameNeeded));
1235                        _namesNeeded.clear(nextNameNeeded); // Stop any other thread from picking this up...
1236                        } while((nextNameNeeded != -1) && (needed.size() < LightweightMetaDataFetchInterface.MAX_NAMES_REQUEST));
1237                    }
1238    
1239                // If no requests found then quit the loop...
1240                if(needed.isEmpty()) { break; }
1241    
1242                // OK, we actually do need to fetch these names...
1243                // Do it outside of the lock scope.
1244                try
1245                    {
1246    if(IsDebug.isDebug) { logger.log("[NAMES: Fetching "+(needed.size())+" names " + needed + "...]"); }
1247                    final int ordinals[] = new int[needed.size()];
1248                    for(int i = ordinals.length; --i >= 0; )
1249                        { ordinals[i] = needed.get(i).intValue(); }
1250                    final Name.ExhibitFull results[] = Utils.getExhibitNames((bs == null) ? null : bs.getCodeBase(), ordinals);
1251                    for(int i = ordinals.length; --i >= 0; )
1252                        {
1253                        final int ordinal = ordinals[i];
1254                        names[ordinal] = results[i];
1255                        // Clear flag given that name has been fetched successfully...
1256                        assert(results[i] != null);
1257                        synchronized(_namesNeeded) { _namesNeeded.clear(ordinal); }
1258                        }
1259                    }
1260                catch(final Exception e) /* Absorb error, but whinge. */
1261                    {
1262                    if(IsDebug.isDebug) { e.printStackTrace(); }
1263                    logger.log("Problem fetching exhibit names, please check your network connection: " + e.getMessage());
1264    
1265                    // Pause a moment in case of a persistent problem
1266                    // to avoid eating all the CPU in a high-priority thread.
1267                    try { Thread.sleep(1001 + Rnd.fastRnd.nextInt(1001)); }
1268                    catch(final InterruptedException ie) { }
1269    
1270                    break; /* After any error, stop trying to fetch more on this round. */
1271                    }
1272                }
1273            }
1274    
1275    
1276    //    /**Save persistent properties (if possible).
1277    //     * If we have PersistenceService then
1278    //     * try to create/allocate space to save (ignoring errors if already there)
1279    //     * and save the data.
1280    //     */
1281    //    private void saveProperties()
1282    //        {
1283    //        if((ps != null) && (bs != null))
1284    //            {
1285    //            try
1286    //                {
1287    //                final URL propsURL = makePropsURL(bs);
1288    //
1289    //                // Try to create a space for persisting the data
1290    //                // if not already present (ignoring error if it is).
1291    //                try { ps.create(propsURL, UploaderProps.MAX_PERS_BYTES); }
1292    //                catch(final IOException e) { }
1293    //
1294    //                // Now try to save our properties data.
1295    //                final FileContents fc = ps.get(propsURL);
1296    //                final OutputStream os = fc.getOutputStream(true);
1297    //                try { os.write(props.getPersistentData()); }
1298    //                finally { os.close(); }
1299    //
1300    //if(IsDebug.isDebug) { logger.log("Saved properties..."); }
1301    //                }
1302    //            catch(final IOException e)
1303    //                {
1304    //                e.printStackTrace();
1305    //                logger.log("Cannot save properties: " + e.getMessage());
1306    //                }
1307    //            }
1308    //        }
1309    
1310        /**Set true when we are shutting down (and never set false again). */
1311        private volatile boolean shuttingDown;
1312    
1313        /**Perform any activity required to shut down cleanly, eg save state.
1314         * This should try to avoid taking a long time.
1315         * <p>
1316         * Package-visible so as to be directly callable by the main GUI class.
1317         */
1318        void shutdown()
1319            {
1320            shuttingDown = true;
1321    
1322    //        // Save any state...
1323    //        saveProperties();
1324            }
1325        }