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 }