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.util;
031
032 import java.io.IOException;
033 import java.text.DateFormat;
034 import java.util.ArrayList;
035 import java.util.Arrays;
036 import java.util.BitSet;
037 import java.util.Collections;
038 import java.util.Comparator;
039 import java.util.Date;
040 import java.util.Iterator;
041 import java.util.List;
042 import java.util.Random;
043 import java.util.RandomAccess;
044 import java.util.Set;
045 import java.util.SortedMap;
046 import java.util.SortedSet;
047 import java.util.TreeMap;
048 import java.util.concurrent.locks.ReentrantLock;
049
050 import javax.servlet.ServletContext;
051 import javax.servlet.http.HttpServletRequest;
052
053 import org.hd.d.pg2k.ai.scorer.ScorerCacheIF;
054 import org.hd.d.pg2k.svrCore.AllExhibitProperties;
055 import org.hd.d.pg2k.svrCore.CS8Bit;
056 import org.hd.d.pg2k.svrCore.Compact7BitString;
057 import org.hd.d.pg2k.svrCore.CoreConsts;
058 import org.hd.d.pg2k.svrCore.ExhibitAttrUtils;
059 import org.hd.d.pg2k.svrCore.ExhibitName;
060 import org.hd.d.pg2k.svrCore.ExhibitPropsComputableMutable;
061 import org.hd.d.pg2k.svrCore.ExhibitStaticAttr;
062 import org.hd.d.pg2k.svrCore.ExhibitThumbnails;
063 import org.hd.d.pg2k.svrCore.GenUtils;
064 import org.hd.d.pg2k.svrCore.LocaleBeanBase;
065 import org.hd.d.pg2k.svrCore.MemoryTools;
066 import org.hd.d.pg2k.svrCore.Name;
067 import org.hd.d.pg2k.svrCore.Name.ExhibitFull;
068 import org.hd.d.pg2k.svrCore.Rnd;
069 import org.hd.d.pg2k.svrCore.TextUtils;
070 import org.hd.d.pg2k.svrCore.ThreadUtils;
071 import org.hd.d.pg2k.svrCore.Tuple;
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.props.GenProps;
075 import org.hd.d.pg2k.svrCore.uploader.UploaderUtils;
076 import org.hd.d.pg2k.svrCore.vars.EventPeriod;
077 import org.hd.d.pg2k.svrCore.vars.EventVariableValue;
078 import org.hd.d.pg2k.svrCore.vars.SimpleVariableDefinition;
079 import org.hd.d.pg2k.svrCore.vars.SystemVariables;
080 import org.hd.d.pg2k.webSvr.exhibit.BuiltInFilters;
081 import org.hd.d.pg2k.webSvr.exhibit.DataSourceBean;
082 import org.hd.d.pg2k.webSvr.exhibit.FilterBean;
083 import org.hd.d.pg2k.webSvr.exhibit.FilterExpr;
084 import org.hd.d.pg2k.webSvr.exhibit.FilterIF;
085 import org.hd.d.pg2k.webSvr.exhibit.SortExpr;
086
087 import ORG.hd.d.IsDebug;
088
089 /**Utility functions to generate HTML inserts for showing batches of thumbnails/links.
090 * One advantage of having a method here rather than in-line in a JSP
091 * is that this code is pre-compiled off-line for speed and robustness.
092 */
093 public final class HTMLThumbnailInsertGenerators
094 {
095 /**Prevent construction of an instance. */
096 private HTMLThumbnailInsertGenerators() { }
097
098
099 /**Minimum width for column display in pixels; strictly positive. */
100 public static final int COLDISP_MIN_WIDTH_PIXELS =
101 ExhibitThumbnails.SML_STATIC_IMAGE_TN_LDIM_PX +
102 2 * PageSkinUtils.GENERIC_TN_BORDER_WIDTH_PIXELS +
103 10; // Fudge factor.
104
105 /**Minimum height for column display in pixels; strictly positive.
106 * Taken to be the height of display for one exhibit
107 * with a small thumbnail.
108 */
109 public static final int COLDISP_MIN_HEIGHT_PIXELS =
110 ExhibitThumbnails.SML_STATIC_IMAGE_TN_LDIM_PX +
111 2 * PageSkinUtils.GENERIC_TN_BORDER_WIDTH_PIXELS +
112 90; // Fudge factor.
113
114 /**Assumed approximate mean pixels wide per (small) character; strictly positive. */
115 public static final int COLDISP_MEAN_CHAR_WIDTH_PIXELS = 9;
116
117 /**Assumed approximate mean pixels wide per (small) character; strictly positive. */
118 public static final int COLDISP_MEAN_CHAR_HEIGHT_PIXELS = 14;
119
120 /**Cap number of items cached for thumbnail generators for efficiency; strictly positive.
121 * Somewhat more than just the "first page" of 'best exhibit' results
122 * so that we can show some exhibits beyond it.
123 */
124 private static final int MAX_THUMBNAIL_EXHIBITS_TO_RETAIN_FOR_DISPLAY = 2*WebConsts.SINGLE_PAGE_CONTACT_SHEET_TN_COUNT + 1;
125
126 /**Key to retrieve AEP-linked set of 'random JPEG' exhibits. */
127 private static final DataSourceBean.AEPLinkedKey rndKey = new DataSourceBean.AEPLinkedKey("random JPEG exhibits key");
128
129 /**Key to retrieve AEP-linked set of 'best' exhibits. */
130 private static final DataSourceBean.AEPLinkedKey bestKey = new DataSourceBean.AEPLinkedKey("best exhibits key");
131
132 /**Lock to prevent more than one update run on _bestE at once. */
133 private static final ReentrantLock _bestE_update_lock = new ReentrantLock();
134
135 /**Time before which we will not attempt incremental update of _bestE set.
136 * Set so as to limit fraction of system CPU spent recomputing/updating.
137 * <p>
138 * Is volatile so as to allow access without a lock.
139 * <p>
140 * Private to getBestExhibitsSelection().
141 */
142 private static volatile long _bestE_dontUpdateBefore;
143
144 /**Makes HTML for row display of the exhibits supplied; never null.
145 * @param exhibits non-null list of full, valid exhibit names,
146 * more important first (later items may not be displayed)
147 * @param dataSource non-null source of exhibit data and thumbnails
148 * @param reduceEffort if true, reduce CPU and bandwidth load
149 * in executing this routine and implied by the output
150 * @return empty output if no thumbnails available, else wrappable linked HTML for thumbnail display
151 */
152 public static String makeHTMLExhibitRow(final Name.ExhibitFull exhibits[],
153 final DataSourceBean dataSource,
154 final LocaleBeanBase localeBean,
155 final boolean reduceEffort)
156 throws IOException
157 {
158 // Try to get all thumbnails pre-loaded/computed up-front, concurrently.
159 final BitSet exists = WebUtils.exhibitsHaveThumbnail(dataSource,
160 Arrays.asList(exhibits),
161 false, // Small thumbnails.
162 !reduceEffort);
163
164 final int n = exhibits.length;
165 final StringBuilder result = new StringBuilder(n << 7);
166
167 // Get the exhibit properties.
168 final AllExhibitProperties aep = dataSource.getAllExhibitProperties(-1);
169
170 for(int i = 0; i < n; ++i)
171 {
172 if(!exists.get(i)) { continue; }
173
174 final Name.ExhibitFull exhibitName = exhibits[i];
175
176 // Get this exhibit's properties; never null.
177 final ExhibitStaticAttr esa = aep.aeid.getStaticAttr(exhibitName);
178
179 // Skip entry for a no-longer-extant exhibit,
180 // eg after an AEP change.
181 if(esa == null)
182 { continue; }
183
184 // Get the exhibit type...
185 //final ExhibitMIME.ExhibitTypeParameters exhibitType = (ExhibitMIME.getInputFileType(esa.filePath));
186
187 // Compute the relative URL to the catalogue page; never null.
188 final String catPage = WebUtils.makeCatPageRRURL(exhibitName,
189 WebConsts.F_secondary_generated_HTML_suffix);
190
191 // Try and get a thumbnail, which must be an inlineable image.
192 final java.awt.Dimension thumbnailXyDim = new java.awt.Dimension();
193 final String thumbnailURL = (!exists.get(i)) ? null :
194 WebUtils.makeHTMLInlineImageThumbnailURL(
195 dataSource,
196 exhibitName,
197 false, // Get a small thumbnail.
198 false, // Allow CDN URL.
199 thumbnailXyDim,
200 reduceEffort);
201
202 // If we have a thumbnail then insert it.
203 if(thumbnailURL != null)
204 {
205 // AKA for title...
206 final CharSequence specificAKA = GenUtils.getLocalisedTreeDesc(aep, exhibitName, localeBean, false, true, false, true);
207 final boolean hasSpecificAKA = (specificAKA.length() != 0);
208
209 result.
210 append("<a href=\"").append(catPage).append("\" target=\"_top\">").
211 append("<img src=\"").append(thumbnailURL).append('\"').
212 append(" width=").append(thumbnailXyDim.width).
213 append(" height=").append(thumbnailXyDim.height).
214 append(" alt=").append(UploaderUtils.quoteHTMLArg(localeBean.getLocalisedMessage("common.thumbnail")));
215 if(hasSpecificAKA) { result.append(" title=\"").append(specificAKA.toString().replace('"', '\'')).append('"'); }
216 result.append(WebConsts.TN_IMG_TAG_EXTRA_ATTR).append('>').
217 append("</a>");
218
219 result.append(' '); // Insert some space to allow wrapping, etc.
220 }
221 }
222
223 return(result.toString());
224 }
225
226
227 /**Makes HTML for columnar (ie taller and thin) display of exhibits; never null.
228 * Given the width and height for display,
229 * and a list of exhibits,
230 * will format and show as many as is reasonably possible in the space.
231 * Exhibits from the start of the list will be shown first.
232 * <p>
233 * Text is horizontally centred.
234 * <p>
235 * This may not show any exhibits at all if the area is too small.
236 * <p>
237 * This will not work well if the space is very narrow or shallow,
238 * much less than about 100 pixels.
239 * <p>
240 * Note that this may not get things exactly right if the user
241 * selects different font sizes in their browser.
242 *
243 * @param exhibits non-null list of full, valid exhibit names,
244 * more important first (later items may not be displayed)
245 * @param dataSource non-null source of exhibit data and thumbnails
246 * @param pixelsWidth non-negative width of display space in pixels
247 * @param pixelsHeight non-negative height of display space in pixels
248 * @param reduceEffort if true, reduce CPU and bandwidth load
249 * in executing this routine and implied by the output
250 *
251 * @throws java.io.IOException if there is difficulty retrieving meta-data
252 */
253 public static String makeHTMLExhibitColumnDisplay(final Name.ExhibitFull exhibits[],
254 final int pixelsWidth,
255 final int pixelsHeight,
256 final DataSourceBean dataSource,
257 final LocaleBeanBase localeBean,
258 final boolean reduceEffort)
259 throws IOException
260 {
261 if((exhibits == null) ||
262 (pixelsWidth < 0) ||
263 (pixelsHeight < 0) ||
264 (dataSource == null) ||
265 (localeBean == null))
266 { throw new IllegalArgumentException(); }
267
268 // If too narrow then return an empty string.
269 if((pixelsWidth < COLDISP_MIN_WIDTH_PIXELS) ||
270 (pixelsHeight < COLDISP_MIN_HEIGHT_PIXELS))
271 { return(""); }
272
273 // Compute maximum number of exhibits we could display vertically.
274 final int maxVertical = pixelsHeight / COLDISP_MIN_HEIGHT_PIXELS;
275
276 // Compute the actual number of exhibits we will show.
277 final int toShow = Math.min(maxVertical, exhibits.length);
278 assert(toShow > 0);
279
280 // Try to get all thumbnails pre-loaded/computed up-front, concurrently.
281 final BitSet exists = WebUtils.exhibitsHaveThumbnail(dataSource,
282 Arrays.asList(exhibits).subList(0, toShow),
283 false, // Small thumbnails.
284 !reduceEffort);
285
286 // Get the exhibit properties.
287 final AllExhibitProperties aep = dataSource.getAllExhibitProperties(-1);
288
289 final StringBuilder result = new StringBuilder(exhibits.length * 101);
290 result.append("<table align=center cellpadding=0 cellspacing=0>");
291
292 for(int i = 0; i < toShow; ++i)
293 {
294 final Name.ExhibitFull exhibitName = exhibits[i];
295
296 // Check for a bad exhibit name...
297 if(exhibitName == null)
298 { throw new IllegalArgumentException(); }
299
300 // Get this exhibit's properties; never null.
301 final ExhibitStaticAttr esa = aep.aeid.getStaticAttr(exhibitName);
302
303 // Skip entry for a no-longer-extant exhibit,
304 // eg after an AEP change.
305 if(esa == null)
306 { continue; }
307
308 // Start new row for new exhibit.
309 result.append("<tr><td align=center height=").
310 append(COLDISP_MIN_HEIGHT_PIXELS).
311 append('>');
312
313 // Get the exhibit type...
314 final ExhibitMIME.ExhibitTypeParameters exhibitType = (ExhibitMIME.getInputFileType(esa.getCharSequence()));
315
316 // Compute the relative URL to the catalogue page; never null.
317 final String catPage = WebUtils.makeCatPageRRURL(exhibitName,
318 WebConsts.F_secondary_generated_HTML_suffix);
319
320 // Try and get a thumbnail, which must be an inlineable image.
321 final java.awt.Dimension thumbnailXyDim = new java.awt.Dimension();
322 final String thumbnailURL = (!exists.get(i)) ? null :
323 WebUtils.makeHTMLInlineImageThumbnailURL(
324 dataSource,
325 exhibitName,
326 false, // Get a small thumbnail.
327 false, // Allow CDN URL.
328 thumbnailXyDim,
329 reduceEffort);
330
331 // If we have a thumbnail, insert it.
332 if(thumbnailURL != null)
333 {
334 // AKA for title...
335 final CharSequence specificAKA = GenUtils.getLocalisedTreeDesc(aep, exhibitName, localeBean, false, true, false, true);
336 final boolean hasSpecificAKA = (specificAKA.length() != 0);
337
338 result.append("<table align=center border=").
339 append(PageSkinUtils.GENERIC_TN_BORDER_WIDTH_PIXELS).
340 append("><tr><td>").
341 append("<a href=\"").append(catPage).append("\" target=_top>").
342 append("<img src=\"").append(thumbnailURL).append('\"').
343 append(" width=").append(thumbnailXyDim.width).
344 append(" height=").append(thumbnailXyDim.height).
345 append(" alt=").append(UploaderUtils.quoteHTMLArg(localeBean.getLocalisedMessage("common.thumbnail")));
346 if(hasSpecificAKA) { result.append(" title=\"").append(specificAKA.toString().replace('"', '\'')).append('"'); }
347 result.append(WebConsts.TN_IMG_TAG_EXTRA_ATTR).append('>').
348 append("</a></td></tr></table>");
349 }
350 // Put in some summary info in lieu of a thumbnail.
351 else
352 {
353 if(exhibitType != null)
354 { result.append(exhibitType.description); }
355 }
356
357 // Force line-break after thumbnail.
358 result.append("<br />");
359
360 // Link to the catalogue page.
361 result.append("<a href=\"").
362 append(catPage).
363 append("\" target=_top>").
364 append(TextUtils.sanitiseForXML(ExhibitName.getFileComponent(exhibitName).toString(),
365 pixelsWidth / COLDISP_MEAN_CHAR_WIDTH_PIXELS,
366 false)).
367 append("</a>");
368
369 // Force line-break after link.
370 result.append("<br />");
371
372 // Show size and date.
373 result.append(TextUtils.sizeAsText(esa.length, true)).append("; ");
374 result.append("<nobr>").
375 append(DateFormat.getDateInstance(DateFormat.MEDIUM, localeBean.getLocale()).format(new Date(esa.timestamp))).
376 append("</nobr>");
377
378 result.append("</td></tr>");
379 }
380
381 result.append("</table>");
382 return(result.toString());
383 }
384
385 /**Empty (immutable) array to return to indicate no results. */
386 private static final Name.ExhibitFull[] NO_RESULTS = new Name.ExhibitFull[0];
387
388 /**Key to retrieve AEP-linked set of 'new' exhibits. */
389 private static final DataSourceBean.AEPLinkedKey newKey = new DataSourceBean.AEPLinkedKey("new exhibits key");
390
391 /**Get random selection of new-ish exhibit full names; never null.
392 * Selects from newest few percent of available exhibits,
393 * and within those the very newest are most likely to be returned.
394 * Note that no absolute age/time limits are imposed.
395 * <p>
396 * If there are any exhibits at all,
397 * and maxExhibits is positive,
398 * this routine ensures that at least one exhibit is a candidate to show.
399 * <p>
400 * These results are cached against the AEP,
401 * and automatically discarded/recomputed when the exhibit set changes.
402 * <p>
403 * May return empty list, but no nulls nor duplicates.
404 * <p>
405 * Should only be used with one servlet context.
406 *
407 * @param application the servlet context; never null
408 * @param maxExhibits the maximum number of exhibits to return;
409 * non-negative
410 * @param rnd the random number source if non-null, else null to pick top items
411 */
412 public static Name.ExhibitFull[] getNewExhibitSelection(final ServletContext application,
413 final int maxExhibits,
414 final Random rnd)
415 throws IOException
416 { return(getNewExhibitSelection(application, maxExhibits, rnd, false)); }
417
418 /**Get random selection of new-ish exhibit full names; never null.
419 * Selects from newest few percent of available exhibits,
420 * and within those the very newest are most likely to be returned.
421 * Note that no absolute age/time limits are imposed.
422 * <p>
423 * If there are any exhibits at all,
424 * and maxExhibits is positive,
425 * this routine ensures that at least one exhibit is a candidate to show.
426 * <p>
427 * These results are cached against the AEP,
428 * and automatically discarded/recomputed when the exhibit set changes.
429 * <p>
430 * May return empty list, but no nulls nor duplicates.
431 * <p>
432 * Should only be used with one servlet context.
433 *
434 * @param application the servlet context; never null
435 * @param maxExhibits the maximum number of exhibits to return;
436 * non-negative
437 * @param rnd the random number source if non-null, else null to pick top items
438 * @param beQuick if true and the result is needs recomputing from scratch
439 * then this returns an empty result and attempts to recompute in the background
440 */
441 public static Name.ExhibitFull[] getNewExhibitSelection(final ServletContext application,
442 final int maxExhibits,
443 final Random rnd,
444 final boolean beQuick)
445 throws IOException
446 {
447 if((application == null) ||
448 (maxExhibits < 0))
449 { throw new IllegalArgumentException(); }
450
451 // Retrieve AEP-linked cache.
452 final DataSourceBean dsb = DataSourceBean.getApplicationInstance(application);
453 if(dsb == null) { throw new IllegalStateException(); }
454 Name.ExhibitFull[] newE;
455 while(null == (newE = (Name.ExhibitFull[]) dsb.getAEPLinkedValue(newKey)))
456 {
457 // If being quick and no result is to hand
458 // then attempt to start computation in the background
459 // and return an empty result...
460 if(beQuick)
461 {
462 ThreadUtils.lowPriorityThreadPoolDiscardable.execute(new Runnable(){
463 public final void run()
464 {
465 // Note: we don't pass in the caller's 'Random' instance and request only a minimal result size.
466 try { getNewExhibitSelection(application, 1, null, false); }
467 catch(final IOException e) { };
468 }
469 });
470 return(NO_RESULTS);
471 }
472
473 // Extract newest exhibits.
474 final AllExhibitProperties aep = dsb.getAllExhibitProperties(-1);
475 final SortedSet<ExhibitStaticAttr> ss = GenUtils.leastN(
476 Math.min(aep.aeid.length/2, MAX_THUMBNAIL_EXHIBITS_TO_RETAIN_FOR_DISPLAY),
477 aep.aeid.getAllStaticAttrs().iterator(),
478 (new Comparator<ExhibitStaticAttr>(){
479 public final int compare(final ExhibitStaticAttr e1, final ExhibitStaticAttr e2)
480 {
481 if(e1.timestamp > e2.timestamp) { return(-1); } // Correct (newest first).
482 if(e1.timestamp < e2.timestamp) { return(+1); } // Wrong.
483 return(e1.compareTo(e2)); // Same age: break ties by exhibit name.
484 }
485 }));
486 final int len = ss.size();
487 assert(len <= MAX_THUMBNAIL_EXHIBITS_TO_RETAIN_FOR_DISPLAY);
488
489 newE = new Name.ExhibitFull[len];
490 final Iterator<ExhibitStaticAttr> it = ss.iterator();
491 for(int i = 0; i < len; ++i)
492 { newE[i] = it.next().getExhibitFullName(); }
493
494 // Cache raw newest-first array...
495 dsb.putIfAbsentAEPLinkedValue(newKey, newE);
496 }
497
498 final int candidates = newE.length;
499
500 // If no suitable exhibits at all,
501 // then return immediately.
502 if(candidates < 1) { return(NO_RESULTS); }
503
504 // Pick a starting exhibit.
505 // (Forced to be zero if no more exhibits than requested, or no random source.)
506 final int which = ((rnd == null) || (candidates <= maxExhibits)) ?
507 0 : rnd.nextInt(1 + rnd.nextInt(1 + candidates - maxExhibits));
508
509 // Return a suitable-size chunk from the selected starting point.
510 return(Arrays.copyOfRange(newE, which, which + Math.min(candidates, maxExhibits)));
511 }
512
513 /**Get random selection of JPEG exhibit full names; never null.
514 * These results are cached against the AEP,
515 * and automatically discarded/recomputed when the exhibit set changes.
516 * <p>
517 * May return empty list, but no nulls nor duplicates.
518 * <p>
519 * Should only be used with one servlet context.
520 *
521 * @param application the servlet context; never null
522 * @param maxExhibits the maximum number of exhibits to return;
523 * non-negative
524 */
525 public static Name.ExhibitFull[] getRandomJPEGSelection(final ServletContext application,
526 final int maxExhibits)
527 throws IOException
528 { return(getRandomJPEGSelection(application, maxExhibits, false)); }
529
530 /**Get random selection of JPEG exhibit full names; never null.
531 * These results are cached against the AEP,
532 * and automatically discarded/recomputed when the exhibit set changes.
533 * <p>
534 * May return empty list, but no nulls nor duplicates.
535 * <p>
536 * Should only be used with one servlet context.
537 *
538 * @param application the servlet context; never null
539 * @param maxExhibits the maximum number of exhibits to return;
540 * non-negative
541 * @param beQuick if true and the result is needs recomputing from scratch
542 * then this returns an empty result and attempts to recompute in the background
543 */
544 public static Name.ExhibitFull[] getRandomJPEGSelection(final ServletContext application,
545 final int maxExhibits,
546 final boolean beQuick)
547 throws IOException
548 {
549 if((application == null) ||
550 (maxExhibits < 0))
551 { throw new IllegalArgumentException(); }
552
553 // Retrieve AEP-linked cache, creating it if necessary.
554 final DataSourceBean dsb = DataSourceBean.getApplicationInstance(application);
555 if(dsb == null) { throw new IllegalStateException(); }
556 FilterBean rndJPEGE;
557 while(null == (rndJPEGE = (FilterBean) dsb.getAEPLinkedValue(rndKey)))
558 {
559 rndJPEGE = new FilterBean();
560
561 // Don't retain this data if the system is very short of memory... Should be quick to recompute...
562 rndJPEGE.setMemorySensitiveCache(true);
563
564 // Set a name for diagnostics...
565 rndJPEGE.setName("random JPEG");
566
567 // We filter this (on creation) to be the best N (to improve locality) that are:
568 // * JPEG images.
569 // * Too large to be thumbnails themselves (ie not very small).
570 rndJPEGE.setExpr(new SortExpr(new FilterExpr(null, (new FilterIF()
571 {
572 final public boolean accept(final AllExhibitProperties aep, final Name.ExhibitFull exhibitName)
573 {
574 final ExhibitStaticAttr esa = aep.aeid.getStaticAttr(exhibitName);
575 if(null == esa) { return(false); }
576
577 // Quick-n-dirty filter to exclude very small images...
578 if(esa.length <= ExhibitThumbnails.STD_ABS_MAX_BYTES)
579 { return (false); }
580
581 // Quick filter to exclude non-JPEG exhibits.
582 final ExhibitMIME.ExhibitTypeParameters et = (ExhibitMIME.getInputFileType(esa.getCharSequence()));
583 if((et == null) || (et.type != ExhibitMIME.ET_JPEG))
584 { return (false); }
585
586 return(true); // Looks OK...
587 }
588 /**Unique Serialisation class ID generated by http://random.hd.org/. */
589 private static final long serialVersionUID = -5173930708308579233L;
590 })),
591 // Limit number of distinct items so as to improve locality in various system caches
592 // and save a little memory retaining the collection.
593 new BuiltInFilters.sortByGoodness(Math.max(503, MAX_THUMBNAIL_EXHIBITS_TO_RETAIN_FOR_DISPLAY))));
594
595 // Set this to automatically time out and recompute periodically.
596 // Aim to be at about the default system temporal slackness
597 // or the computable-mutable recalculation time if less.
598 // If this timeout is very small then there may be a performance impact.
599 // If too high then we will not reflect changes in popularity quickly.
600 rndJPEGE.setExpiryInterval(Math.min(2*CoreConsts.DEFAULT_TEMPORAL_SLACKNESS_S,
601 ExhibitPropsComputableMutable.MAX_AGE_BEFORE_STALE_MS/2503) *
602 (500 + Rnd.fastRnd.nextInt(1000)));
603
604 // Atomically cache the new bean.
605 dsb.putIfAbsentAEPLinkedValue(rndKey, rndJPEGE);
606 }
607
608 // Get full list of "good" exhibits best first.
609 final List<Name.ExhibitFull> exhibits = beQuick ?
610 rndJPEGE.peek(application) : rndJPEGE.select(application);
611 // If beQuick==true and nothing immediately ready then return immediately
612 // but also attempt to do (re)compute in the background.
613 if(exhibits == null)
614 {
615 ThreadUtils.lowPriorityThreadPoolDiscardable.execute(new Runnable(){
616 public final void run()
617 {
618 // Note: we request only a minimal result size.
619 try { getRandomJPEGSelection(application, 1, false); }
620 catch(final IOException e) { };
621 }
622 });
623 return(NO_RESULTS);
624 }
625
626 final int ne = exhibits.size();
627
628 // If no suitable exhibits at all,
629 // then return immediately.
630 if(ne < 1) { return(NO_RESULTS); }
631
632 final Name.ExhibitFull result[] = new Name.ExhibitFull[Math.min(maxExhibits, ne)];
633 // Pick a starting exhibit...
634 int which = Rnd.fastRnd.nextInt(ne);
635 int loc = 0;
636
637 // ...and wrap around from it...
638 for(int i = result.length; --i >= 0; )
639 {
640 final Name.ExhibitFull exhibitName = exhibits.get(ne - 1 - which);
641 result[loc++] = exhibitName;
642 if(++which >= ne) { which = 0; }
643 }
644
645 return(result);
646 }
647
648
649 /**If true then invalidate "best" collection if any stale value is found.
650 * Otherwise, only invalidate if there is a significant value change.
651 * (If not then we can survive until the next periodic re-sort.)
652 * This is may be pretty aggressive on CPU and other resources if true.
653 */
654 private static final boolean CHECK_ALL_FOR_STALE = false;
655
656 /**Out-of-order tolerance we will allow before forcing "best" selection to be recomputed.
657 * This corresponds to an insignificantly small difference
658 * that will rarely be noticed by a (non-obsessive!) human,
659 * so need not force us to invalidate and re-sort the entire collection.
660 */
661 private static final int BEST_EPSILON = Integer.MAX_VALUE / (1 + WebConsts.SINGLE_PAGE_CONTACT_SHEET_TN_COUNT);
662
663 /**Compute hash for best exhibits (non-null) name array result of getBestExhibitSelection().
664 * Suitable for use in weak ETag.
665 * <p>
666 * Built from the name hash of the first and last items as indicative of the entire list.
667 * <p>
668 * Non-negative; zero if the array is zero-length.
669 */
670 public static final long computeBestExhibitSelectionHash(final Name.ExhibitFull[] names)
671 {
672 if(null == names) { throw new IllegalArgumentException(); }
673
674 final int length = names.length;
675 if(0 == length) { return(0); }
676 return(((((long) names[0].hashCode()) << 31L) ^ (long) names[length-1].hashCode()) & (~0L >>> 1));
677 }
678
679 /**Get names of the the best exhibits ("goodness" greater than zero); never null.
680 * These results are cached against the AEP,
681 * and automatically discarded/recomputed when the exhibit set changes,
682 * and also periodically to allow for the values changing over time.
683 * <p>
684 * May return an empty list, but no nulls nor duplicates.
685 * <p>
686 * Should only be used with one servlet context.
687 * <p>
688 * If a random number source is supplied then it is used to chose
689 * a random (though weighted toward best) selection of exhibits.
690 * <p>
691 * If no random number source is supplied then the very best exhibits
692 * are returned.
693 * <p>
694 * (May invalidate its cache and spend a short time updating entries
695 * if any of the items that it is about to return is stale
696 * or the results are no longer in order (best first)
697 * in the hope that non-stale ones will have been computed in the mean time.
698 * This incremental cache validation and recomputation should help
699 * keep the results reasonable without penalising any one caller too much,
700 * and returns reasonable results in a short time on each call.)
701 *
702 * @param application the servlet context; never null
703 * @param maxExhibits the maximum number of exhibits to return;
704 * non-negative
705 * @param rnd the random number source if non-null, else null to pick top items
706 */
707 public static Name.ExhibitFull[] getBestExhibitSelection(final ServletContext application,
708 final int maxExhibits,
709 final Random rnd)
710 throws IOException
711 { return(getBestExhibitSelection(application, maxExhibits, rnd, false)); }
712
713 /**Get names of the the best exhibits ("goodness" greater than zero); never null.
714 * These results are cached against the AEP,
715 * and automatically discarded/recomputed when the exhibit set changes,
716 * and also periodically to allow for the values changing over time.
717 * <p>
718 * May return an empty list, but no nulls nor duplicates.
719 * <p>
720 * Should only be used with one servlet context.
721 * <p>
722 * If a random number source is supplied then it is used to chose
723 * a random (though weighted toward best) selection of exhibits.
724 * <p>
725 * If no random number source is supplied then the very best exhibits
726 * are returned.
727 * <p>
728 * (May invalidate its cache and spend a short time updating entries
729 * if any of the items that it is about to return is stale
730 * or the results are no longer in order (best first)
731 * in the hope that non-stale ones will have been computed in the mean time.
732 * This incremental cache validation and recomputation should help
733 * keep the results reasonable without penalising any one caller too much,
734 * and returns reasonable results in a short time on each call.)
735 * <p>
736 * This ties not to block for the calculation of any EPCM values.
737 *
738 * @param application the servlet context; never null
739 * @param maxExhibits the maximum number of exhibits to return;
740 * non-negative
741 * @param rnd the random number source if non-null, else null to pick top items
742 * @param beQuick if true and the result is needs recomputing from scratch
743 * then this returns an empty result and attempts to recompute in the background
744 */
745 public static Name.ExhibitFull[] getBestExhibitSelection(final ServletContext application,
746 final int maxExhibits,
747 final Random rnd,
748 final boolean beQuick)
749 throws IOException
750 {
751 if((application == null) ||
752 (maxExhibits < 0))
753 { throw new IllegalArgumentException(); }
754
755 // Note start time of this call,
756 // as simply fetching/updating the results may take some time.
757 final long startTime = System.currentTimeMillis();
758
759 // Retrieve AEP-linked cache, creating it if necessary.
760 final DataSourceBean dsb = DataSourceBean.getApplicationInstance(application);
761 if(dsb == null) { throw new IllegalStateException(); }
762 FilterBean bestE;
763 while(null == (bestE = (FilterBean) dsb.getAEPLinkedValue(bestKey)))
764 {
765 bestE = new FilterBean();
766
767 // Hold onto this data since it is expensive to recompute and is capped in size.
768 bestE.setMemorySensitiveCache(false);
769
770 // Set a name for diagnostics...
771 bestE.setName("best exhibits");
772
773 // Dummy/default for quick EPCM approximation.
774 final GenProps gp = new GenProps();
775
776 // We filter this (on creation) to be:
777 // * Exhibits better than average/neutral "goodness".
778 // We sort this to be best (highest-goodness) first.
779 bestE.setExpr(new SortExpr(new FilterExpr(null, (new FilterIF()
780 {
781 final public boolean accept(final AllExhibitProperties aep, final Name.ExhibitFull exhibitName)
782 {
783 // Make do with whatever value is already computed,
784 // though force at least a fast approximation if at all possible
785 // to bootstrap the process and provide some differentiation.
786 final ExhibitPropsComputableMutable epcm =
787 aep.getExhibitPropsComputableMutable(exhibitName, true, gp, null, null);
788
789 // True only if better than neutral.
790 // We don't care if the value is stale
791 // (on the grounds that it may still be a reasonable approximation)
792 // but we reject entirely unknown/uncomputed values.
793 if((epcm != null) && (epcm.getGoodness() > 0))
794 { return(true); }
795
796 // Not good enough.
797 return(false);
798 }
799
800 /**Unique Serialisation class ID generated by http://random.hd.org/. */
801 private static final long serialVersionUID = 671635642661710967L;
802 })),
803 // Limit number of items to be considered "best" for efficiency,
804 // to somewhat more than just the "first page" of 'best exhibit' results
805 // so that we can show some exhibits beyond it.
806 new BuiltInFilters.sortByGoodness(MAX_THUMBNAIL_EXHIBITS_TO_RETAIN_FOR_DISPLAY)));
807
808 // Set this to automatically time out and recompute periodically.
809 // Aim to be at about the default system temporal slackness
810 // or the computable-mutable recalculation time if less.
811 // If this timeout is very small then there may be a performance impact.
812 // If too high then we will not reflect changes in popularity quickly.
813 bestE.setExpiryInterval(Math.min(2*CoreConsts.DEFAULT_TEMPORAL_SLACKNESS_S,
814 ExhibitPropsComputableMutable.MAX_AGE_BEFORE_STALE_MS/2503) *
815 (500 + Rnd.fastRnd.nextInt(1000)));
816
817 // Atomically cache the new bean.
818 dsb.putIfAbsentAEPLinkedValue(bestKey, bestE);
819 }
820
821 // Get full list of "good" exhibits best first.
822 final List<Name.ExhibitFull> exhibits = beQuick ?
823 bestE.peek(application) : bestE.select(application);
824 // If being quick and no result is to hand
825 // then attempt to start computation in the background
826 // and return an empty result...
827 if(exhibits == null)
828 {
829 ThreadUtils.lowPriorityThreadPoolDiscardable.execute(new Runnable(){
830 public final void run()
831 {
832 // Note: we don't pass in the caller's 'Random' instance and request only a minimal result size.
833 try
834 {
835 final ExhibitFull[] bestExhibitSelection = getBestExhibitSelection(application, 1, null, false);
836
837 // Try to force returned exhibit (if any) EPCM up to date
838 // to help ensure some progress during invalidation cycle.
839 if(bestExhibitSelection.length > 0)
840 { dsb.getAllExhibitProperties(-1).getExhibitPropsComputableMutable(bestExhibitSelection[0], false, dsb.getGenProps(-1), dsb, dsb.getScorerCache()); }
841 }
842 catch(final IOException ignore) { }
843 }
844 });
845 return(NO_RESULTS);
846 }
847
848 final int ne = exhibits.size();
849
850 // If no suitable exhibits at all then return immediately.
851 if(ne < 1)
852 { return(NO_RESULTS); }
853
854 final Name.ExhibitFull result[] = new Name.ExhibitFull[Math.min(maxExhibits, ne)];
855 // Pick a starting exhibit.
856 // (Forced to be zero if no more exhibits than requested, or no random source.)
857 int which = ((rnd == null) || (ne <= maxExhibits)) ?
858 0 : rnd.nextInt(1 + rnd.nextInt(1 + ne - maxExhibits));
859
860 // Copy the exhibits in order to the result array.
861 for(int i = 0; i < result.length; ++i)
862 { result[i] = exhibits.get(which++); }
863
864 // If it is too soon to consider doing an update check
865 // then return our result immediately.
866 if(startTime <= _bestE_dontUpdateBefore) { return(result); }
867
868 // If an update is already in progress
869 // then return our result immediately.
870 if(!_bestE_update_lock.tryLock()) { return(result); }
871 try
872 {
873 // Recompute any stale (or now-not-good) items found
874 // in the list about to be returned
875 // (plus at least one other at random if possible).
876
877 // Try to keep run-time of routine well below page-generation time limit,
878 // indeed well below the time allowed for interactive operations.
879 // The more time that can be spent in one go, however,
880 // the more efficient that any incremental updating will be,
881 // at the potential cost of annoying a waiting/blocked user.
882 final long stopBy = startTime + CoreConsts.MAX_INTERACTIVE_DELAY_MS/2;
883
884 // Check/validate (and try to bring up-to-date)
885 // at least one entry before giving up
886 // so as to try to ensure some progress
887 // if stale entries do need recomputing.
888 // Try to ensure that at least whatever we rated as
889 // the "best" item in this result set
890 // (first in the array) is non-stale by the time we have finished.
891 final AllExhibitProperties aep = dsb.getAllExhibitProperties(-1);
892 final GenProps gp = dsb.getGenProps(-1);
893
894 // Flag to be set non-null if we need to invalidate the cache.
895 String invalidate = null;
896
897 // Compute goodness of current "best" item.
898 final int bestGoodness = aep.getExhibitPropsComputableMutable(exhibits.get(0), false, gp, dsb, dsb.getScorerCache()).getGoodness();
899 // Compute goodness of current "worst-best" item.
900 final int worstGoodness = aep.getExhibitPropsComputableMutable(exhibits.get(ne-1), false, gp, dsb, dsb.getScorerCache()).getGoodness();
901
902 // Some early funnies to check...
903 if((bestGoodness <= 0) || (worstGoodness <= 0) || (worstGoodness > bestGoodness))
904 { invalidate = "best/worst entries invalid or mis-ordered"; }
905
906 // Try to ensure all values for results are non-stale and in order.
907 // We don't care if we had to re-compute an entry
908 // as long as it is still roughly in place.
909 // If conserving energy or the system is heavily loaded then trim the work done here.
910 final boolean conserving = GenUtils.mustConservePower() || ThreadUtils.isCPUHeavilyLoaded();
911 for(int i = 0; i < result.length; ++i) // Check "best" items first.
912 {
913 // Check if this is a stale, missing, or out-of-order item...
914 final Name.ExhibitFull exhibitName = result[i];
915 final ExhibitPropsComputableMutable epcm = aep.getExhibitPropsComputableMutable(
916 exhibitName, true, gp, dsb, dsb.getScorerCache());
917 final int goodness = (epcm == null) ? Integer.MIN_VALUE : epcm.getGoodness();
918 final int precGoodness;
919 if(goodness <= 0)
920 {
921 // Something weird happened and this entry
922 // is missing or become inadmissible,
923 // so we must re-filter and re-sort the cache next time.
924 invalidate = "entry is missing/invalid: " + exhibitName;
925 if(epcm == null) { break; /* Avoid blowing up later tests with NPEs! */ }
926 }
927 // Almost always check that the first item is good; possibly check all.
928 else if((CHECK_ALL_FOR_STALE || (i == 0)) && epcm.isStale() && !conserving)
929 {
930 // Make sure that we recompute our cache next time.
931 invalidate = "top entry had stale EPCM: " + exhibitName;
932 }
933 else if((i < result.length) && (i > 0) &&
934 (goodness > BEST_EPSILON + (long) (precGoodness = aep.getExhibitPropsComputableMutable(result[i-1], true, gp, dsb, dsb.getScorerCache()).getGoodness())))
935 {
936 // Each item in the result array must be
937 // no more "good" than its predecessor
938 // (allowing an "epsilon" tolerance).
939 // If not so, then entries are mis-ordered,
940 // (so must have been recomputed)
941 // then we must re-sort the cache next time.
942 // Note the computation as a long to avoid overflow.
943 invalidate = "entry (goodness="+goodness+") is out of order wrt neighbour (goodness="+precGoodness+"): " + exhibitName + ", index: "+i;
944 }
945 else if((goodness > bestGoodness) || (goodness < worstGoodness))
946 {
947 // If we've found one better than the current "best"
948 // or one worse than the current "worst best"
949 // then we must re-sort the cache next time.
950 invalidate = "entry (goodness="+goodness+") is out of range wrt best/worst ("+bestGoodness+"/"+worstGoodness+"): " + exhibitName + ", index: "+i;
951 }
952
953 // Give up now if we've taken a noticeable time...
954 if(System.currentTimeMillis() > stopBy) { break; }
955 }
956
957 // If we are going to invalidate the whole cache,
958 // then spend a little time trying to bring entries up to date first
959 // to make the recomputation process as efficient as possible.
960 // We try to make sure that even if we run out of time that
961 // we still get good coverage by systematic and random sampling.
962 if(invalidate != null)
963 {
964 // RandomAccess list of all live exhibits.
965 final List<Name.ExhibitFull> allExhibits = aep.aeid.getAllExhibitNamesSorted();
966 assert(allExhibits instanceof RandomAccess); // Fast to access.
967 final int aes = allExhibits.size();
968
969 // If this update itself is being run in the same thread pool used below
970 // then this submit() may well be discarded due to limited pool space.
971 ThreadUtils.lowPriorityThreadPoolDiscardable.submit(new Runnable(){ public final void run() {
972 final ScorerCacheIF scorerCache = dsb.getScorerCache();
973 // Try to ensure that the first and last candidates are always OK.
974 // If the bounds are good and all in between are correctly ordered then our result is fine.
975 aep.getExhibitPropsComputableMutable(exhibits.get(0), false, gp, dsb, scorerCache);
976 if(!conserving)
977 {
978 if(ne > 1)
979 { aep.getExhibitPropsComputableMutable(exhibits.get(ne-1), false, gp, dsb, scorerCache); }
980 // Randomly pick one of the intermediate candidate exhibits.
981 if(ne > 2)
982 { aep.getExhibitPropsComputableMutable(exhibits.get(1+Rnd.fastRnd.nextInt(ne-2)), false, gp, dsb, scorerCache); }
983 }
984 // Randomly pick one of the entire set of live exhibits.
985 if(aes > 0) { aep.getExhibitPropsComputableMutable(allExhibits.get(Rnd.fastRnd.nextInt(aes)), false, gp, dsb, scorerCache); }
986 } });
987
988 // Do cache invalidation at most once,
989 // so as to minimise cost to concurrent threads for example.
990 bestE.invalidate();
991
992 // Log the reason for invalidation...
993 if(IsDebug.isDebug) { dsb.log("Invalidated 'best' collection: " + invalidate); }
994 }
995 }
996 catch(final Exception e)
997 {
998 // Absorb any transient errors from data set shifting beneath us
999 // eg a now-invalid exhibit name may cause an NPE.
1000 dsb.log("WARNING: exception in getBestExhibitSelection(): " + e.getMessage());
1001 // Force recomputation of data in case it is now booby-trapped.
1002 bestE.invalidate();
1003 }
1004 finally
1005 {
1006 // Put off next update to limit overall fraction of CPU time used.
1007 // Note that we only really care about keeping up-to-date
1008 // the entries that are "good".
1009 final long doneAt = System.currentTimeMillis();
1010 final long computeTime = doneAt - startTime;
1011 final long putOffBy = Math.min(13*computeTime, // Keep below ~10% CPU updating this...
1012 69000 + Rnd.fastRnd.nextInt(39503)); // Cap the delay time...
1013 _bestE_dontUpdateBefore = doneAt + putOffBy; /* Volatile update is atomic. */
1014
1015 // Let the lock go now that we're done...
1016 _bestE_update_lock.unlock();
1017 }
1018
1019 return(result);
1020 }
1021
1022
1023 /**If true then always omit from the contact sheet any items that do not immediately/ever have a thumbnail. */
1024 private static final boolean CS_ALWAYS_ONLY_SHOW_THUMBNAILS = false;
1025
1026 /**If true then cache negative ("") contact-sheet cache entries, else recompute such values each time to save memory. */
1027 private static final boolean CS_CACHE_NEGATIVE_EMPTY_RESULTS = false;
1028
1029 /**If true then try to cache cat page contact sheets; this may require minor cosmetic changes in generated HTML for efficiency. */
1030 public static final boolean CS_CACHE_FOR_CAT_PAGES = true;
1031
1032
1033 /**Used as a unique key into the cache for the 'other factors value' (GenProps timestamp, etc) to watch for changes; non-null. */
1034 private static final CS8Bit OFV_KEY = CS8Bit.EMPTY;
1035
1036 /**Private key used by getContactSheetHTML(); never null. */
1037 private static final DataSourceBean.AEPLinkedKey _CSCacheKey = new DataSourceBean.AEPLinkedKey("_CSCacheKey");
1038
1039 /**Make "contact print" / "preview" HTML fragment to show a user all similar exhibits, "" if none possible; never null.
1040 * This routine returns a table of (small thumbnails of) all the exhibits
1041 * with the same main words prefix as the supplied word, in order,
1042 * in as compact and efficient form as reasonable.
1043 * <p>
1044 * Note that if using the 'Hoverbox' feature,
1045 * then overlay standard thumbnails may appear above and to the left or right of
1046 * a small thumbnail being hovered over,
1047 * so sufficient clearance should be given above this HTML insert
1048 * to avoid conflicts with surrounding material, such as ads.
1049 * <p>
1050 * This may choose to omit or show substitute text for exhibits with
1051 * no immediately-available thumbnail.
1052 * <p>
1053 * This does not generate any headings.
1054 * <p>
1055 * If this encounters an error (eg an exception, or inconsistent arguments)
1056 * then it will attempt to return gracefully with a "no-contact-sheet-available"
1057 * empty value ("").
1058 * <p>
1059 * This may spend a limited amount of time trying to create thumbnails,
1060 * bring up-to-date computable data, etc, but in any case tries to be quick.
1061 * <p>
1062 * Appends a special HTML comment tag to the result
1063 * if incomplete due to missing thumbnails or lack of time.
1064 * <p>
1065 * This may cache completed results against all of the exhibit names given,
1066 * to be discarded if the AEP for the supplied DataSourceBean changes,
1067 * or other relevant data changes.
1068 *
1069 * @param exhibitName this name of the main exhibit for which a contact sheet
1070 * is to be generated; must be a valid, non-null exhibit name
1071 * @param specialColour HTML colour name (or #RRGGBB value) to distinguish
1072 * the main exhibit in the contact sheet somehow,
1073 * quoted if it need be for an attribute value,
1074 * may be ignored for efficiency/consistency;
1075 * null if no such marking to be done
1076 * @param dataSource data source for thumbnails, etc; never null
1077 * @param smartSortedNames list of all exhibit names,
1078 * sorted with the SMART_ORDER Comparator,
1079 * and should be random-access else this will probably be horribly slow;
1080 * never null
1081 *
1082 * @return "" if a contact sheet is not applicable or cannot be generated
1083 * (so result is safe to drop into HTML without checking if need be)
1084 * else a well-formed HTML fragment suitable to drop into a catalogue page;
1085 * never null
1086 *
1087 * <p>
1088 * TODO: separate AEP-linked long-lived negatively-cached ("") values from auto-expiring short-lived positive values to improve cache behaviour
1089 */
1090 @SuppressWarnings("unchecked")
1091 public static String getContactSheetHTML(final Name.ExhibitFull exhibitName,
1092 final String specialColour,
1093 final DataSourceBean dataSource,
1094 final List<Name.ExhibitFull> smartSortedNames)
1095 {
1096 if((null == exhibitName) ||
1097 (dataSource == null) ||
1098 (smartSortedNames == null))
1099 { throw new IllegalArgumentException(); }
1100
1101 // Note start time...
1102 final long startTime = System.currentTimeMillis();
1103
1104 final int maxPixelsWidth = PageSkinUtils.MAX_LIGHTBOX_WIDTH_PX;
1105
1106 // try
1107 {
1108 // Compute the (lower-cased) main-words component of the main exhibit.
1109 // We'll potentially display in the contact sheet all exhibits
1110 // with the same (lower-cased to match catalogue sort behaviour) main-words component.
1111 // This is the lookup key in our AEP-linked map of cached results.
1112 final SortedSet<String> attrWords = ExhibitAttrUtils.getAttrWords().getAttrWordsSortedSet();
1113 final CS8Bit mainWordsLC = // Probably marginally more efficient to extract from short name.
1114 new CS8Bit(exhibitName.getShortName().getMainWordsComponent(attrWords).toString().toLowerCase());
1115
1116 // If cacheing then first ensure that our cache map exists...
1117 // The cache, if used, maps from main-words component to HTML.
1118 // The HTML text is stored as a compact text string
1119 // from which the text can be recovered using toString().
1120 // The map is thread-safe and size limited.
1121 // Each entry may be quite large.
1122 // We are prepared to discard the cache entirely if memory is stressed.
1123 // Limit maximum cache size in proportion to heap size; strictly positive.
1124 // Aim to allow ~2000 entries per 1GB of heap space (at initialisation of this instance).
1125 // Caches non-trivial text wrapped as AutoExpirable to force rebuild periodically,
1126 // for example because of embedded CDN URLs of possibly limited lifetime validity.
1127 MemoryTools.CacheMiniMap<CS8Bit,Object> cache;
1128 if(CS_CACHE_FOR_CAT_PAGES)
1129 {
1130 while(null == (cache = (MemoryTools.CacheMiniMap<CS8Bit,Object>) dataSource.getAEPLinkedValue(_CSCacheKey)))
1131 { dataSource.putIfAbsentAEPLinkedValue(_CSCacheKey, SimpleLRUMap.<CS8Bit,Object>create(Math.max(1<<5, (int) Math.min(1<<12, Runtime.getRuntime().totalMemory() >> 18)), _CSCacheKey.comment)); }
1132
1133 // Note that there is a map from a unique key (not a valid MWLC value)
1134 // to current GP timestamp and preferred mirror/CDN URL details.
1135 // If those change then the cache is cleared to avoid having cached egregious embedded URLs.
1136 // Slightly race-prone, but likely only a couple of broken thumbnails at worst...
1137 final GenProps genProps = dataSource.getGenProps(-1);
1138 final Long gpts = Long.valueOf(genProps.timestamp);
1139 final String hostCDN = MirrorSelectionUtils.chooseMirrorHostForHighBandwidth(null, dataSource);
1140 final Tuple.Pair<Long, String> ofv = new Tuple.Pair<Long, String>(gpts, hostCDN);
1141 final Object extantTS = cache.get(OFV_KEY);
1142 if(!ofv.equals(extantTS))
1143 {
1144 if(null != extantTS)
1145 {
1146 if(IsDebug.isDebug) { dataSource.log("getContactSheetHTML(): gp/CDN changed; clearing cache"); }
1147 cache.clear();
1148 }
1149 cache.put(OFV_KEY, ofv);
1150 }
1151 }
1152
1153 // See if we have the value cached already.
1154 // (A "" value indicates an empty result that need not be recomputed.)
1155 // If not then compute it.
1156 final Object cachedValue;
1157 String result;
1158 CS8Bit key = mainWordsLC; // Key into cache...
1159 if(!CS_CACHE_FOR_CAT_PAGES || (null == (cachedValue = cache.get(key))))
1160 {
1161 // This key may be shareable, so intern() it.
1162 if(CS_CACHE_FOR_CAT_PAGES) { key = MemoryTools.intern(key); }
1163
1164 final int ssnLen = smartSortedNames.size();
1165 // We must use a comparator that matches how the names were sorted...
1166 final int ourPos = Collections.binarySearch(smartSortedNames, exhibitName, ExhibitAttrUtils.getAttrWords().SMART_ORDER);
1167 if((ourPos < 0) || (ourPos >= ssnLen))
1168 { return(""); /* The main exhibit cannot be found; exit gracefully and never cache this result. */ }
1169
1170 // Find the first exhibit that matches our main words component (inclusive).
1171 // (We'll ignore case differences here regardless of how the smart-sort works.)
1172 int firstMatch = ourPos;
1173 for(int i = ourPos; --i >= 0; )
1174 {
1175 final CharSequence mwc = smartSortedNames.get(i).getShortName().getMainWordsComponent(attrWords);
1176 if(!TextUtils.contentEqualsIgnoreCase(mwc, mainWordsLC))
1177 { break; }
1178 firstMatch = i; // This matched, so move the limit down...
1179 }
1180
1181 // Now find the last matching exhibit (exclusive).
1182 int lastPos;
1183 for(lastPos = ourPos+1 ; lastPos < ssnLen; ++lastPos)
1184 {
1185 final CharSequence mwc = smartSortedNames.get(lastPos).getShortName().getMainWordsComponent(attrWords);
1186 if(!TextUtils.contentEqualsIgnoreCase(mwc, mainWordsLC))
1187 { break; }
1188 }
1189
1190 // Note sublist of matching entries.
1191 // Take copy if in might be necessary to erase entries...
1192 final List<Name.ExhibitFull> matchingNames = CS_ALWAYS_ONLY_SHOW_THUMBNAILS ?
1193 new ArrayList<Name.ExhibitFull>(smartSortedNames.subList(firstMatch, lastPos)) :
1194 smartSortedNames.subList(firstMatch, lastPos);
1195
1196 // If there is only one matching name
1197 // then it is the main exhibit,
1198 // and therefore there is no contact sheet to show.
1199 if(matchingNames.size() <= 1)
1200 {
1201 if(CS_CACHE_FOR_CAT_PAGES && CS_CACHE_NEGATIVE_EMPTY_RESULTS) { cache.put(key, ""); }
1202 return(""); /* No sheet to show. */
1203 }
1204
1205 // If we only want to show in our sheet
1206 // thumbnails available here and now,
1207 // then weed any others out of our list.
1208 if(CS_ALWAYS_ONLY_SHOW_THUMBNAILS)
1209 {
1210 for(final Iterator<Name.ExhibitFull> it = matchingNames.iterator(); it.hasNext(); )
1211 {
1212 final Name.ExhibitFull ex = it.next();
1213 // Zap items without an extant thumbnail...
1214 if(!WebUtils.exhibitHasThumbnail(dataSource, ex, false, false))
1215 { it.remove(); }
1216 }
1217
1218 // If nothing is left in our list
1219 // then return an empty sheet immediately.
1220 if(matchingNames.isEmpty())
1221 {
1222 if(CS_CACHE_FOR_CAT_PAGES && CS_CACHE_NEGATIVE_EMPTY_RESULTS) { cache.put(key, ""); }
1223 return(""); /* No sheet to show. */
1224 }
1225 // If the only thing left in our list is our main exhibit
1226 // then return an empty sheet immediately.
1227 if((matchingNames.size() == 1) && exhibitName.equals(matchingNames.get(0)))
1228 {
1229 if(CS_CACHE_FOR_CAT_PAGES && CS_CACHE_NEGATIVE_EMPTY_RESULTS) { cache.put(key, ""); }
1230 return(""); /* No sheet to show. */
1231 }
1232 }
1233
1234 // Target time to stop doing optional computations by;
1235 // probably short enough to avoid annoying interactive users
1236 // while giving us the opportunity to get most/all thumbnails
1237 // in the contact sheet.
1238 final long stopBy = startTime + WebConsts.MAX_PG_DOWNLOAD_MS/2;
1239
1240 // Return our generated HTML fragment...
1241 result = generateGenericContactSheetHTML(maxPixelsWidth, matchingNames, CS_CACHE_FOR_CAT_PAGES ? null : specialColour, exhibitName, stopBy, dataSource);
1242 final boolean finished = !result.endsWith(CS_INCOMPLETE);
1243
1244 if(IsDebug.isDebug && (System.currentTimeMillis() > stopBy)) { dataSource.log("INFO: getContactSheetHTML(): overran computing contact sheet (matching="+matchingNames.size()+", finished="+finished+") for "+exhibitName); }
1245
1246 // If we are cacheing and the result is complete,
1247 // and there's plenty of free memory at the moment,
1248 // then cache/capture the result, in a compact form if possible.
1249 if(CS_CACHE_FOR_CAT_PAGES && finished && MemoryTools.lotsFree())
1250 {
1251 // There is no point intern()ing this result text
1252 // since we assume that it will be unique for each key except for trivial cases.
1253 // Cache the text wrapped as an AutoExpirable with limited lifetime
1254 // to gradually reclaim unused entries automatically
1255 // and avoid hanging on to embedded CDN-related URLs too long (they may become invalid).
1256 Object optionallyCompacted = result;
1257 try { optionallyCompacted = Compact7BitString.convertToCompact7BitString(result, null); }
1258 catch(final IllegalArgumentException e)
1259 {
1260 /* Not 7-bit text, so fall through to cache as uncompacted String. */
1261 if(IsDebug.isDebug) { dataSource.log("getContactSheetHTML(): COULD NOT COMPACT contact sheet non-7-bit-text for "+exhibitName, e); }
1262 }
1263 final Object toCache = optionallyCompacted;
1264 // Put in cache, wrapped to expire automatically.
1265 cache.put(key, new MemoryTools.AutoExpirableFixedLifeBase(WebConsts.DEFAULT_PAGE_CACHE_MS << 1)
1266 { @Override public String toString() { return(toCache.toString()); } });
1267 if(IsDebug.isDebug) { dataSource.log("getContactSheetHTML(): cached result of "+result.length()+" chars for "+exhibitName); }
1268 }
1269 }
1270 // Use the (non-null) cached value.
1271 else { result = cachedValue.toString(); }
1272
1273 return(result);
1274 }
1275 // catch(final IOException e)
1276 // {
1277 // // Error encountered, so give up gracefully.
1278 // return("");
1279 // }
1280 }
1281
1282 /**Request-level attribute set non-null before creating the header to load Hoverbox CSS. */
1283 private static final String REQ_ATTR_NAME_LOAD_HOVERBOX_CCS = "org.hd.pg2k.enableHB";
1284
1285 /**Call this before generating the header to load the Hoverbox CSS.
1286 * Does nothing if the `Hoverbox' is not enabled.
1287 * <p>
1288 * This is idempotent, and sets a request-level attribute.
1289 */
1290 public static final void enableHoverboxCSS(final HttpServletRequest request)
1291 {
1292 // Do nothing if Hoverbox is not enabled.
1293 if(!WebConsts.SUPPORT_HOVERBOX) { return; }
1294 request.setAttribute(REQ_ATTR_NAME_LOAD_HOVERBOX_CCS, Boolean.TRUE);
1295 }
1296
1297 /**Returns true if the the Hoverbox CSS has been enabled with enableHoverboxCSS().
1298 * Does nothing if the `Hoverbox' is not enabled.
1299 * <p>
1300 * This checks a request-level attribute.
1301 */
1302 public static final boolean hoverboxCSSIsEnabled(final HttpServletRequest request)
1303 {
1304 // Always false if Hoverbox is not enabled.
1305 if(!WebConsts.SUPPORT_HOVERBOX) { return(false); }
1306 // Check if the attribute has been set non-null.
1307 return(null != request.getAttribute(REQ_ATTR_NAME_LOAD_HOVERBOX_CCS));
1308 }
1309
1310 /**Unique comment tag at end of HTML for contact sheet if we ran out of time or it is otherwise incomplete. */
1311 public static final String CS_INCOMPLETE = "<!--INCOMPLETE-->";
1312
1313 /**Maximum that 'Hoverbox' overlay thumbnail may extend outside the table containing small thumbnails, in pixels; strictly positive.
1314 * This should match up with the Hoverbox CSS targets
1315 * and possibly a fudge-factor for various browsers' rendering quirks.
1316 */
1317 private static final int MAX_HOVERBOX_OVERSPILL_PX = 192 - 16;
1318
1319 /**If padding a lightbox out with a overlay image safety area, do we insert popular links? */
1320 private static final boolean LIGHTBOX_PADDING_POP_LINKS = true;
1321
1322 /**Allow this many pixels' border in lightbox table; non-negative. */
1323 private static final int LIGHTBOX_TABLE_BORDER_PX = 1;
1324
1325 /**Generate contact sheet of the exhibits given, in order; never null.
1326 * Has special HTML comment tag at end to indicate if the result
1327 * is incomplete due to missing thumbnails or lack of time.
1328 * <p>
1329 * Note that if using the 'Hoverbox' feature,
1330 * then overlay larger thumbnails may appear above a thumbnail being hovered over,
1331 * so we insert sufficient padding around the thumbnail table
1332 * to avoid conflicts with surrounding page material, such as ads.
1333 *
1334 * @param exhibitNames in-order list of exhibits to display; never null
1335 * @param specialExhibitName name of "special" exhibit to emphasise;
1336 * null if none
1337 * @param stopBy approximate target time to finish by;
1338 * can be set in the past to generate HTML as quickly as possible
1339 * @param specialColour HTML colour name (or #RRGGBB value) to distinguish
1340 * the main exhibit in the contact sheet somehow,
1341 * quoted if it need be for an attribute value;
1342 * null if no such marking to be done
1343 * @param maxPixelsWidth maximum pixels available for display (approx);
1344 * strictly positive
1345 * @param dataSource data source for thumbnails, etc; never null
1346 * @return HTML fragment for contact sheet ending with CS_INCOMPLETE to show incomplete, or "";
1347 * never null
1348 */
1349 public static String generateGenericContactSheetHTML(final int maxPixelsWidth,
1350 final List<Name.ExhibitFull> exhibitNames,
1351 final String specialColour,
1352 final Name.ExhibitFull specialExhibitName,
1353 final long stopBy,
1354 final DataSourceBean dataSource)
1355 {
1356 if((maxPixelsWidth <= 0) ||
1357 (exhibitNames == null) ||
1358 (dataSource == null))
1359 { throw new IllegalArgumentException(); }
1360
1361 final int nExhibits = exhibitNames.size();
1362 if(nExhibits == 0)
1363 { return(""); }
1364
1365 try
1366 {
1367 final AllExhibitProperties aep = dataSource.getAllExhibitProperties(-1);
1368
1369 // Find out all those thumbnails whose status is unknown in the CDN.
1370 // TODO: we might be able to use CDN-hosted thumbnails even without local copies...
1371 final Set<Name.ExhibitFull> unknownCDNStatus =
1372 CDNUtils.findTNsWithUnknownCDNStatus(dataSource, exhibitNames, true);
1373
1374 // Find out what small thumbnails are to hand locally right now,
1375 // possibly forcing their creation.
1376 final BitSet thumbnailExists = WebUtils.exhibitsHaveThumbnail(dataSource, exhibitNames, false, true);
1377 // If supporting 'hoverbox' then we want to know about standard thumbnails too.
1378 // We don't try to force the creation of these...
1379 final BitSet stdThumbnailExists = (!WebConsts.SUPPORT_HOVERBOX) ? null :
1380 WebUtils.exhibitsHaveThumbnail(dataSource, exhibitNames, true, false);
1381 // Note if this lightbox is 'hoverable', ie if using the 'Hoverbox' feature
1382 // and if any thumbnails are capable of being hovered over
1383 // (ie both small and std sizes are available for any one exhibit).
1384 final boolean hoverable = WebConsts.SUPPORT_HOVERBOX && thumbnailExists.intersects(stdThumbnailExists);
1385
1386 // Allow for Hoverbox 'overspill' padding around the lightbox if necessary.
1387 final int innerMaxPixelsWidth = (!hoverable) ? maxPixelsWidth :
1388 (maxPixelsWidth - MAX_HOVERBOX_OVERSPILL_PX);
1389
1390 // Calculate how many columns of thumbnails we may be able to show in this lightbox.
1391 // We assume that each thumbnail may be its maximum size in both dimensions.
1392 // We always show at least one column.
1393 final int tnSize = ExhibitThumbnails.SML_STATIC_IMAGE_TN_LDIM_PX;
1394 final int colWidth = (tnSize + 2*LIGHTBOX_TABLE_BORDER_PX);
1395 // Try to use an even number of columns if possible.
1396 final int maxCols = Math.max(1, (innerMaxPixelsWidth / colWidth) & ~1);
1397 final int actualCols = Math.min(maxCols, nExhibits);
1398 final int actualRows = (nExhibits + (maxCols-1)) / maxCols;
1399
1400 final int _capacity = 256 + (nExhibits * 1024);
1401 // Generate the HTML as a table.
1402 final StringBuilder result = new StringBuilder(_capacity);
1403 boolean firstRow = true; // Are we producing the first row?
1404 int columnNumber = 0; // Which column are we about to fill?
1405
1406 // Wrap a safety buffer zone around the lightbox if hoverable.
1407 if(hoverable)
1408 {
1409 // Filler text to put in the side safety zone, if large enough...
1410 // But don't spend ages doing this.
1411 final String popularLinks = ((!LIGHTBOX_PADDING_POP_LINKS) || (actualRows <= 2)) ? "" :
1412 popularUsefulLinksNarrowCol(dataSource,
1413 (actualRows-1) * tnSize, // Slightly conservative height...
1414 3*(MAX_HOVERBOX_OVERSPILL_PX/4), // Slightly conservative width...
1415 false, // Link to HTML catalogue pages.
1416 stopBy);
1417 // If the lightbox is not tall enough or we can't generate any popular links
1418 // then use a default filler text.
1419 // Attempt to have the links ignored for ad targeting, etc...
1420 final String fillerText = popularLinks.isEmpty() ? ">>>" :
1421 ("<!-- google_ad_section_start(weight=ignore) -->" +
1422 "<big><strong>+</strong></big><br />" +
1423 popularLinks +
1424 "<!-- google_ad_section_end -->");
1425
1426 result.append("<table border=0 cellpadding=0>").
1427 append("<tr align=center><th width=").append(MAX_HOVERBOX_OVERSPILL_PX).
1428 append(" height=").append(MAX_HOVERBOX_OVERSPILL_PX).
1429 append("><big>light box</big></th><td>(Hover over thumbnail for enlarged view.)</td></tr>").
1430 append("<tr align=center><td>").append(fillerText).append("</td><td>");
1431 }
1432
1433 // Start the table...
1434 // We omit quotes around attribute values to save as much bandwidth as possible.
1435 result.append("<table cellpadding=0 border=").append(LIGHTBOX_TABLE_BORDER_PX).
1436 // Try to help the browser to lay out the table quickly...
1437 append(" cols=").append(actualCols).
1438 append(" width=").append(actualCols * colWidth);
1439 if(hoverable) { result.append(" class=hoverbox"); }
1440 result.append('>');
1441
1442 // Start the first row.
1443 final String rowStartHTML = "<tr height="+tnSize+" align=center>";
1444 result.append(rowStartHTML);
1445
1446 final SortedSet<String> attrWords = ExhibitAttrUtils.getAttrWords().getAttrWordsSortedSet();
1447
1448 // Set true if we ran out of time
1449 // or couldn't get a thumbnail that should be available.
1450 boolean incomplete = false;
1451
1452 // If more than a very small fraction have unknown statuses
1453 // then treat the generated HTML as incomplete and not to be cached.
1454 // At this point attempt discovery of any with unknown status.
1455 // (We don't want be be forcing browsers to load the same thumbnails
1456 // from CDN in one place and this server in another.)
1457 if(unknownCDNStatus.size() > (exhibitNames.size() >> 4))
1458 { incomplete = true; }
1459
1460 boolean reduceEffort = false;
1461
1462 for(int i = 0; i < nExhibits; ++i)
1463 {
1464 final Name.ExhibitFull ex = exhibitNames.get(i);
1465
1466 // If we have run off the end of this row,
1467 // then finish this row and start a new one,
1468 // and note that we are not on the first row any more.
1469 if(columnNumber++ >= maxCols)
1470 {
1471 result.append("</tr>").append(rowStartHTML);
1472 columnNumber = 1; // Note that we are just about to fill column 0 of the new row below...
1473 firstRow = false;
1474 }
1475
1476 // Make this cell with a link to the target catalogue page.
1477 // On the first row only, force the width too.
1478 // Mark the main exhibit with a special background colour if so requested.
1479 result.append("<td");
1480 if(firstRow)
1481 { result.append(" width=").append(tnSize); }
1482 if((specialColour != null) && (ex.equals(specialExhibitName)))
1483 { result.append(" bgcolor=").append(specialColour); }
1484 result.append('>');
1485
1486 // Extract part of name that distinguishes this exhibit from its peers.
1487 final int bp[] = ExhibitName.getMainAndAttrWordComponentBoundaries(ex, attrWords);
1488 // Make unique-ish summary with first word and tail attributes...
1489 final String uniquePart = ExhibitName.getMainWords(ex, attrWords).nextElement() + " ~ " + ex.subSequence(bp[1]+1, ex.length());
1490 // Make unique-ish bit that is easier to word-wrap.
1491 final String uWW = uniquePart.replace(ExhibitName.WORD_SEP, ' ');
1492
1493 // Have we run out of time and need to reduce effort spent?
1494 // Once true, stays true for this call.
1495 if(!reduceEffort)
1496 { reduceEffort = System.currentTimeMillis() >= stopBy; }
1497
1498 // Compute the relative URL to the catalogue page; never null.
1499 final String catPage = WebUtils.makeCatPageRRURL(ex,
1500 WebConsts.F_secondary_generated_HTML_suffix);
1501
1502 // Try and create HTML for an extant thumbnail,
1503 // which must be an HTML inlineable image/object.
1504 final java.awt.Dimension thumbnailXyDim = new java.awt.Dimension();
1505 final String thumbnailURL = (!thumbnailExists.get(i)) ? null :
1506 WebUtils.makeHTMLInlineImageThumbnailURL(
1507 dataSource,
1508 ex,
1509 false, // Get a small thumbnail.
1510 false, // Allow CDN URL.
1511 thumbnailXyDim,
1512 reduceEffort);
1513
1514 // If we have a thumbnail then insert it, wrapped as a link.
1515 if(thumbnailURL != null)
1516 {
1517 // If doing 'Hoverbox' then we need an 'li' CSS positioning anchor.
1518 if(hoverable) { result.append("<ul><li>"); }
1519 final String quotedAltText = UploaderUtils.quoteHTMLArg(uWW);
1520 result.append("<a href=\"").append(catPage).append("\">").
1521 // Insert small thumbnail...
1522 append("<img src=\"").append(thumbnailURL).append('\"').
1523 append(" border=0").
1524 append(" width=").append(thumbnailXyDim.width).
1525 append(" height=").append(thumbnailXyDim.height).
1526 append(" alt=").append(quotedAltText).
1527 append(" title=").append(quotedAltText).
1528 append('>');
1529 // If we are supporting Hoverbox AND we have a standard thumbnail
1530 // then put it here with class "preview" to be undisplayed until hovered over.
1531 if(hoverable && stdThumbnailExists.get(i))
1532 {
1533 final java.awt.Dimension thumbnailStdXyDim = new java.awt.Dimension();
1534 final String thumbnailStdRRURL =
1535 WebUtils.makeHTMLInlineImageThumbnailURL(
1536 dataSource,
1537 ex,
1538 true, // Get a std thumbnail.
1539 false, // Allow CDN URL.
1540 thumbnailStdXyDim,
1541 true);
1542 if(thumbnailStdRRURL != null)
1543 {
1544 result.append("<img src=\"").append(thumbnailStdRRURL).append('\"').
1545 append(" class=preview").
1546 append(" border=2").
1547 append(" width=").append(thumbnailStdXyDim.width).
1548 append(" height=").append(thumbnailStdXyDim.height).
1549 append(" alt=").append(quotedAltText).
1550 append(" title=").append(quotedAltText).
1551 append('>');
1552 }
1553 }
1554 result.append("</a>");
1555 if(hoverable) { result.append("</li></ul>"); }
1556 }
1557 // Show some summary info in lieu of a thumbnail.
1558 else
1559 {
1560 // Get this exhibit's properties; never null.
1561 final ExhibitStaticAttr esa = aep.aeid.getStaticAttr(ex);
1562
1563 // Get the exhibit type...
1564 final ExhibitMIME.ExhibitTypeParameters exhibitType =
1565 (esa == null) ? null : (ExhibitMIME.getInputFileType(esa.getCharSequence()));
1566
1567 result.append("<small>");
1568
1569 if(exhibitType != null)
1570 { result.append(exhibitType.description).append("<br />"); }
1571
1572 result.append("<a href=\"").append(catPage).append("\"><i>");
1573 result.append(uWW); // Make word-wrap possible on (eg) FF1.
1574 result.append("</i></a>");
1575 result.append("</small>");
1576
1577 // If this apparently *should* have had a thumbnail
1578 // then mark as our result as incomplete.
1579 if((exhibitType != null) && (exhibitType.canPossiblyCreateThumbnailOfSameMIMEType()))
1580 { incomplete = true; }
1581 }
1582
1583 result.append("</td>");
1584 }
1585
1586 // End the last row...
1587 result.append("</tr>");
1588
1589 // End the table...
1590 result.append("</table>");
1591
1592 // End the wrapper table for Hoverbox, if necessary.
1593 if(hoverable) { result.append("</td></tr></table>"); }
1594
1595 // If we had to reduce effort (and thus not compute the best-possible result)
1596 // then mark the result as incomplete so that we can try again.
1597 if(incomplete)
1598 { result.append(CS_INCOMPLETE); }
1599
1600 if(IsDebug.isDebug && (result.length() > _capacity)) { System.err.println("WARNING: generateGenericContactSheetHTML(): _capacity="+_capacity+" length()="+result.length()+" for exhibit count="+nExhibits); }
1601
1602 return(result.toString());
1603 }
1604 catch(final Exception e)
1605 {
1606 return(""); // In case of error return an empty result.
1607 }
1608 }
1609
1610 /**Generate a narrow column of popular/useful internal links for HTML/XHTML, "" if none; never null.
1611 * Puts a default reasonable cap on generation time
1612 * as a fraction of maximum page-generation time.
1613 * <p>
1614 * May cache/reuse these results for identical size/format parameter for a while for speed.
1615 *
1616 * @param dsb data source; never null
1617 * @param pixelsHigh approx number of pixels high; strictly positive
1618 * @param pixelsWide approx number of pixels wide; strictly positive
1619 * @param toXHTML if true then link to XHTML catalogue pages rather than HTML pages
1620 */
1621 public static String popularUsefulLinksNarrowCol(final DataSourceBean dsb,
1622 final int pixelsHigh,
1623 final int pixelsWide,
1624 final boolean toXHTML)
1625 {
1626 return(popularUsefulLinksNarrowCol(dsb, pixelsHigh, pixelsWide, toXHTML,
1627 System.currentTimeMillis() + 1 + (WebConsts.MAX_PG_DOWNLOAD_MS>>4)));
1628 }
1629
1630 /**Generate a narrow column of popular/useful internal links for HTML/XHTML, "" if none; never null.
1631 * TODO: cache/reuse these results for identical size/format parameter for a while for speed.
1632 *
1633 * @param dsb data source; never null
1634 * @param pixelsHigh approx number of pixels high; strictly positive
1635 * @param pixelsWide approx number of pixels wide; strictly positive
1636 * @param toXHTML if true then link to XHTML catalogue pages rather than HTML pages
1637 * @param stopBy try to stop by the specified time.
1638 */
1639 public static String popularUsefulLinksNarrowCol(final DataSourceBean dsb,
1640 final int pixelsHigh,
1641 final int pixelsWide,
1642 final boolean toXHTML,
1643 final long stopBy)
1644 {
1645 if((dsb == null) ||
1646 (pixelsHigh <= 0) || (pixelsWide <= 0))
1647 { throw new IllegalArgumentException(); }
1648
1649 try
1650 {
1651 final AllExhibitProperties aep = dsb.getAllExhibitProperties(-1);
1652 final GenProps gp = dsb.getGenProps(-1);
1653
1654 final int maxLinks = pixelsHigh / COLDISP_MEAN_CHAR_HEIGHT_PIXELS;
1655 final int maxChars = pixelsWide / COLDISP_MEAN_CHAR_WIDTH_PIXELS;
1656
1657 // If no space to show anything then return...
1658 if((maxLinks < 1) || (maxChars < 1)) { return(""); }
1659
1660 // Collect map from sorted clipped link text to (full) names of exhibits
1661 // to link to catalogue pages for.
1662 // These are kept sorted in a reasonable order.
1663 final SortedMap<String, Name.ExhibitFull> exhibits = new TreeMap<String, Name.ExhibitFull>(String.CASE_INSENSITIVE_ORDER);
1664
1665 // Periods/intervals' data that we will use, in order.
1666 final long currentInterval = EventPeriod.VLONG.getIntervalNumber(System.currentTimeMillis());
1667 final long periods[] =
1668 {
1669 currentInterval-1, // Previous period (cheap, stable, reasonably topical).
1670 currentInterval, // Current period (more topical, more expensive, more noisy).
1671 0, // "All" period (less topical, more expensive, more comprehensive).
1672 };
1673
1674 // System events that we will use, in order.
1675 // Randomise these a little each time.
1676 final SimpleVariableDefinition defs[] =
1677 {
1678 (Rnd.fastRnd.nextBoolean() ?
1679 SystemVariables.ACCESSPATTERN_CLICKTHROUGH : // Usefulness to users and possible profit to us!
1680 SystemVariables.ACCESSPATTERN_COMPLETED_DOWNLOAD_LOCAL), // Local popularity.
1681 (Rnd.fastRnd.nextBoolean() ?
1682 SystemVariables.ACCESSPATTERN_COMPLETED_DOWNLOAD : // Pure popularity.
1683 SystemVariables.ACCESSPATTERN_CAT_PAGE_VIEW), // Pure popularity.
1684 };
1685
1686 // Pick items from further down the lists as we go...
1687 // Only to show the best item for a given displayed text/link...
1688 topLoop: for(int rank = 0; rank < maxLinks; ++rank)
1689 {
1690 for(int p = 0; p < periods.length; ++p)
1691 {
1692 for(final SimpleVariableDefinition def : defs)
1693 {
1694 // Once we have enough links then stop collecting.
1695 if(exhibits.size() >= maxLinks) { break topLoop; }
1696 // If we've taken too long then stop collecting.
1697 if(System.currentTimeMillis() > stopBy) { break topLoop; }
1698
1699 final EventVariableValue[] eventValues = dsb.getEventValues(def, EventPeriod.VLONG, periods[p], null);
1700 if((eventValues.length == 0) || (eventValues[0] == null)) { continue; }
1701 final EventVariableValue eventValue = eventValues[0];
1702 if(rank >= eventValue.getTotalDistinctValues()) { continue; }
1703
1704 // Only use the exhibit if it seems (still) to be valid.
1705 final Object exhibitShortName = eventValue.getValueByRank(rank);
1706 if(!(exhibitShortName instanceof String)) { continue; }
1707 final String sn = (String) exhibitShortName;
1708 // Skip potentially-sensitive subjects.
1709 if(GenUtils.isSensitive(sn, gp)) { continue; }
1710 // Extract the full name.
1711 final Name.ExhibitFull fullName = aep.aeid.getFullName(sn);
1712 if(fullName != null)
1713 {
1714 final String linkText = TextUtils.sanitiseForXML(sn, maxChars, false);
1715
1716 // If we have a different extant item with this link text
1717 // then keep that with the better score.
1718 // Do NOT force an expensive recomputation of either score.
1719 final Name.ExhibitFull extant = exhibits.get(linkText);
1720 if((extant != null) && !extant.equals(fullName))
1721 {
1722 try
1723 {
1724 if(aep.getExhibitPropsComputableMutable(extant, true, gp, dsb, dsb.getScorerCache()).getGoodness() >=
1725 aep.getExhibitPropsComputableMutable(fullName, true, gp, dsb, dsb.getScorerCache()).getGoodness())
1726 { continue; /* New exhibit is no better than extant. */ }
1727 }
1728 // Allow for transients/races causing NPEs, etc.
1729 catch(final Exception e)
1730 { continue; /* In case of error keep extant value. */ }
1731 }
1732
1733 // Use the new value.
1734 exhibits.put(linkText, fullName);
1735 }
1736 }
1737 }
1738 }
1739
1740 if(exhibits.size() == 0) { return(""); } // Nothing to show...
1741
1742 // Generate the HTML...
1743 final StringBuilder sb = new StringBuilder(32 + exhibits.size()*4*maxChars);
1744 sb.append("<small>");
1745 for(final String linkText : exhibits.keySet())
1746 {
1747 final Name.ExhibitFull fullName = exhibits.get(linkText);
1748
1749 // Should be a currently-extant exhibit.
1750 assert(aep.aeid.isPresent(fullName));
1751
1752 // Compute the relative URL to the catalogue page; never null.
1753 final String catPage = WebUtils.makeCatPageRRURL(fullName,
1754 toXHTML ? WebConsts.F_secondary_generated_XHTML_suffix : WebConsts.F_secondary_generated_HTML_suffix);
1755
1756 // Link to the catalogue page.
1757 sb.append("<a href=\"").
1758 append(catPage).
1759 append("\">").
1760 append(linkText).
1761 append("</a><br />");
1762 }
1763 sb.append("</small>");
1764 return(sb.toString());
1765 }
1766 catch(final Exception e)
1767 {
1768 e.printStackTrace();
1769 return(""); // Give up quietly in case of error.
1770 }
1771 }
1772 }