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    package org.hd.d.pg2k.webSvr.exhibit;
030    
031    import java.util.ArrayList;
032    import java.util.Arrays;
033    import java.util.Collection;
034    import java.util.Collections;
035    import java.util.Comparator;
036    import java.util.HashMap;
037    import java.util.Map;
038    import java.util.Random;
039    import java.util.concurrent.Callable;
040    import java.util.concurrent.ConcurrentHashMap;
041    
042    import org.hd.d.pg2k.svrCore.AllExhibitProperties;
043    import org.hd.d.pg2k.svrCore.ExhibitAttrUtils;
044    import org.hd.d.pg2k.svrCore.ExhibitName;
045    import org.hd.d.pg2k.svrCore.ExhibitStaticAttr;
046    import org.hd.d.pg2k.svrCore.GenUtils;
047    import org.hd.d.pg2k.svrCore.Name;
048    import org.hd.d.pg2k.svrCore.Name.ExhibitFull;
049    import org.hd.d.pg2k.svrCore.Name.ExhibitShort;
050    import org.hd.d.pg2k.svrCore.Rnd;
051    import org.hd.d.pg2k.svrCore.TextUtils;
052    import org.hd.d.pg2k.svrCore.ThreadUtils;
053    import org.hd.d.pg2k.svrCore.location.Location;
054    import org.hd.d.pg2k.svrCore.props.GenProps;
055    
056    /**Built-in filters and sorters that can be looked up by name.
057     * The filters are inner classes and not publicly visible
058     * so that by-and-large they do have to be looked up by name.
059     * There is an internal registry so that reflection doesn't
060     * have to be used and packagers such as dashO should have
061     * any easier time working out what's going on...
062     * <p>
063     * All the filters/sorters are constructed with a String array or arguments.
064     * This argument list is never null, though may be zero length if there are
065     * no arguments for a particular filter/sorter.  None of the arguments
066     * itself will be null.
067     * <p>
068     * The exhibit names are full names, ie including the leading directory component.
069     */
070    public final class BuiltInFilters
071        {
072        /**No one can instantiate this. */
073        private BuiltInFilters() { }
074    
075        /**Accepts all exhibits.
076         */
077        public static final class filtAll implements FilterIF
078            {
079            /**Unique serial ID. */
080            private static final long serialVersionUID = -3538216625320264998L;
081            /**Ignores any arguments.
082             */
083            public filtAll(final String args[]) { }
084            public boolean accept(final AllExhibitProperties aep, final Name.ExhibitFull exhibitName)
085                { return(true); }
086    
087            /**Hash is constant. */
088            @Override
089            public int hashCode() { return(0); }
090            /**All instances are equal (could be a singleton). */
091            @Override
092            public boolean equals(final Object obj) { return(obj instanceof filtAll); }        }
093    
094        /**Rejects all exhibits.
095         */
096        public static final class filtNothing implements FilterIF
097            {
098            /**Unique serial ID. */
099            private static final long serialVersionUID = 7375589684054049955L;
100            /**Ignores any arguments.
101             */
102            public filtNothing(final String args[]) { }
103            public boolean accept(final AllExhibitProperties aep, final Name.ExhibitFull exhibitName)
104                { return(false); }
105    
106            /**Hash is constant. */
107            @Override
108            public int hashCode() { return(0); }
109            /**All instances are equal (could be a singleton). */
110            @Override
111            public boolean equals(final Object obj) { return(obj instanceof filtNothing); }
112            }
113    
114        /**Filter by author.
115         * FIXME: add equals() and hashCode().
116         */
117        public static final class filtByAuthor implements FilterIF
118            {
119            /**Unique serial ID. */
120            private static final long serialVersionUID = 1926524023966666518L;
121            private final String acceptableAuthors[];
122            /**Accepts zero or more author (initials); an exhibit matching any of them is accepted.
123             */
124            public filtByAuthor(final String args[]) { acceptableAuthors = args.clone(); }
125            public boolean accept(final AllExhibitProperties aep, final Name.ExhibitFull exhibitName)
126                {
127                final CharSequence initials = ExhibitName.getAuthorComponent(exhibitName);
128                for(int i = acceptableAuthors.length; --i >= 0; )
129                    { if(TextUtils.contentEquals(initials, acceptableAuthors[i])) { return(true); } }
130                return(false); // No match.
131                }
132            }
133    
134    
135    
136        /**Filter by category.
137         * FIXME: add equals() and hashCode().
138         * <p>
139         * Responsible for ~2% of all normal (non-array) allocations (from ExhibitName.getCategoryComponent(exhibitName)) as of 2012/03/16.
140         */
141        public static final class filtByCategory implements FilterIF
142            {
143            /**Unique serial ID. */
144            private static final long serialVersionUID = -2337407306687756258L;
145            private final String acceptableCategories[];
146            /**Accepts zero or more categories; an exhibit matching any of them is accepted. */
147            public filtByCategory(final String args[]) { acceptableCategories = args.clone(); }
148            public boolean accept(final AllExhibitProperties aep, final Name.ExhibitFull exhibitName)
149                {
150                final int nl = exhibitName.length();
151                //final CharSequence category = ExhibitName.getCategoryComponent(exhibitName);
152                byCategory: for(int i = acceptableCategories.length; --i >= 0; )
153                    {
154                    //if(TextUtils.contentEquals(category, acceptableCategories[i])) { return(true); }
155                    // Avoid memory churn implied by making wrapper with CharSequence category = ExhibitName.getCategoryComponent(exhibitName),
156                    // and avoid recursion from futile matches on early characters of Name.ExhibitFull if terminating '/' not present in right place,
157                    // and check the chars in reverse order.
158                    final String acceptableCategory = acceptableCategories[i];
159                    final int cl = acceptableCategory.length();
160                    if(nl < cl + ExhibitName.MIN_FILENAME_LENGTH) { continue; }
161                    if(exhibitName.charAt(cl) != ExhibitName.DIR_SEP) { continue; }
162                    //if(TextUtils.regionMatches(acceptableCategory, 0, exhibitName, 0, cl)) { return(true); }
163                    for(int j = cl; --j >= 0; )
164                        { if(exhibitName.charAt(j) != acceptableCategory.charAt(j)) { continue byCategory; } }
165                    return(true); // All matches.
166                    }
167                return(false); // No match.
168                }
169            }
170    
171    
172        /**Accepts an exhibit with the given case-sensitive substring in its full pathname.
173         * This is no respecter of syntax, or aliases.
174         * <p>
175         * Not fast and does not look at the data or meta-data.
176         * <p>
177         * As a special case this is publicly visible until expression
178         * parsing is implemented.
179         * <p>
180         * FIXME: add equals() and hashCode().
181         */
182        public   static final class filtSimpleSubstringMatch implements FilterIF
183            {
184            /**Unique serial ID. */
185            private static final long serialVersionUID = -5197565900747314559L;
186            /**Takes exactly one argument; the case-sensitive substring to match.
187             */
188            public   filtSimpleSubstringMatch(final String args[])
189                {
190                if((args.length != 1) /* || (args[0] == null) */)
191                    { throw new IllegalArgumentException(); }
192                subString = args[0];
193                }
194            /**The substring to match. */
195            private final String subString;
196            public boolean accept(final AllExhibitProperties aep, final Name.ExhibitFull exhibitName)
197                { return(exhibitName.toString().indexOf(subString) != -1); } // FIXME: get rid of expensive String conversion
198            }
199    
200        /**Accepts an that has an Estd location within the target area; possibly within the specified area.
201         */
202        public   static final class filtByEstdLocationCentre implements FilterIF
203            {
204            /**Unique serial ID. */
205            private static final long serialVersionUID = 1246204565122236630L;
206            /**Particular location to filter by.
207             * If non-null, item must fall within the area delimited by this
208             * location to be considered.
209             */
210            private final Location.Estd area;
211    
212            /**Takes zero or one arguments; the (optional) area and bounds to filter else any Estd location is acceptable.
213             */
214            public   filtByEstdLocationCentre(final String args[])
215                {
216                if((args != null) || (args.length >= 0))
217                    { throw new Error("NOT IMPLEMENTED"); }
218                area = null;
219                }
220    
221            /**Takes area and bounds to filter for; if null any Estd location is acceptable.
222             */
223            public   filtByEstdLocationCentre(final Location.Estd filterArea)
224                {
225                area = filterArea;
226                }
227    
228            public boolean accept(final AllExhibitProperties aep, final Name.ExhibitFull exhibitName)
229                {
230                final Location.Base l = aep.getLocation(exhibitName);
231                if(!(l instanceof Location.Estd))
232                    { return(false); } // Wrong type or no location info.
233    
234                // Filter on area if so requested.
235                if(area != null)
236                    {
237                    final Location.Estd el = (Location.Estd) l;
238                    if(!area.containsCentre(el))
239                        { return(false); } // Centre not in specified area.
240                    }
241    
242                return(true); // Seems OK.
243                }
244    
245            /**Filters are equal if area objects are (or are both null). */
246            @Override
247            public boolean equals(final Object obj)
248                {
249                if(!(obj instanceof filtByEstdLocationCentre)) { return(false); }
250                final filtByEstdLocationCentre other = (filtByEstdLocationCentre) obj;
251                if(area == null) { return(other.area == null); }
252                return(area.equals(other.area));
253                }
254    
255            /**Filter has hashcode of its area; 0 if none. */
256            @Override
257            public int hashCode()
258                {
259                if(area == null) { return(0); }
260                return(area.hashCode());
261                }
262            }
263    
264    
265    
266        /**Accepts an exhibit with the given case-sensitive substring starting its final (file / short-name) component.
267         * This is not much of a respector of syntax.
268         * <p>
269         * Very fast and does not look at the data or meta-data.
270         * <p>
271         * As a special case this is publicly visible until expression
272         * parsing is implemented.
273         * <p>
274         * FIXME: add equals() and hashCode().
275         */
276        public   static final class filtPrefixMatch implements FilterIF
277            {
278            /**Unique serial ID. */
279            private static final long serialVersionUID = 8580496714859968839L;
280            /**Takes exactly one argument; the case-sensitive substring to match. */
281            public   filtPrefixMatch(final String args[])
282                {
283                if((args.length != 1) /* || (args[0] == null) */)
284                    { throw new IllegalArgumentException(); }
285                prefix = args[0];
286                }
287            /**The substring to match. */
288            private final String prefix;
289            public boolean accept(final AllExhibitProperties aep, final Name.ExhibitFull exhibitName)
290                {
291                final ExhibitShort shortName = exhibitName.getShortName();
292                return(TextUtils.startsWith(shortName, prefix));
293    //            return(shortName.toString().startsWith(prefix));
294                }
295            }
296    
297        /**Accepts an exhibit with the given case-sensitive substring ending its final component.
298         * This is not much of a respector of syntax.
299         * <p>
300         * Very fast and does not look at the data or meta-data.
301         * <p>
302         * As a special case this is publicly visible until expression
303         * parsing is implemented.
304         * <p>
305         * FIXME: add equals() and hashCode().
306         */
307        public   static final class filtSimpleSuffixMatch implements FilterIF
308            {
309            /**Unique serial ID. */
310            private static final long serialVersionUID = -6968644372319333517L;
311            /**Takes exactly one argument; the case-sensitive substring to match.
312             */
313            public   filtSimpleSuffixMatch(final String args[])
314                {
315                if((args.length != 1) /* || (args[0] == null) */)
316                    { throw new IllegalArgumentException(); }
317                suffix = args[0];
318                }
319            /**The substring to match. */
320            private final String suffix;
321            public boolean accept(final AllExhibitProperties aep, final Name.ExhibitFull exhibitName)
322                {
323                return(TextUtils.endsWith(exhibitName, suffix));
324    //            return(ExhibitName.getFileComponent(exhibitName).toString().endsWith(suffix));
325                }
326            }
327    
328    
329        /**Reverses the existing order of exhibits in situ.
330         * Takes no parameters, so all instances of this class are equal.
331         */
332        public   static final class sortReverse implements SortIF
333            {
334            /**Unique ID. */
335            private static final long serialVersionUID = -3097396087855039990L;
336            /**Ignores its arguments. */
337            public   sortReverse(final String args[]) { this(); }
338            /**Programmatic constructor. */
339            public   sortReverse() { }
340            /**All instances of this class are identical. */
341            @Override
342            public boolean equals(final Object obj) { return((obj instanceof sortReverse)); }
343            /**Hash is constant. */
344            @Override
345            public int hashCode() { return(-99); }
346    
347            /**Reverses the order of its arguments in situ. */
348            public ExhibitFull[] sort(final AllExhibitProperties aep,
349                                 final Name.ExhibitFull[] exhibitNames)
350                {
351                Collections.reverse(Arrays.asList(exhibitNames));
352                return(exhibitNames);
353                }
354            }
355    
356        /**Selects N exhibits at random and randomises their order.
357         * If the number of exhibits is less than N then this returns
358         * all the exhibits.
359         * <p>
360         * The first value is the unsigned, positive, base-ten integer value
361         * of N.
362         * <p>
363         * If the second argument is missing or null then the random number
364         * sequence used is taken differently each time we run, else the
365         * hash of the second string is taken as the random number seed.
366         */
367        public   static final class sortRandomN implements SortIF
368            {
369            /**Unique serial ID. */
370            private static final long serialVersionUID = 2078921830149773203L;
371    
372            /**Takes exactly one or two arguments.
373             * The first argument is the strictly-positive N.
374             * <p>
375             * The second argument, if absent or null, indicates that
376             * a different random number sequence is used on each run.
377             * If present and non-null then its hash is used to seed a
378             * random number generator to give repeatable results.
379             */
380            public   sortRandomN(final String args[])
381                {
382                this(Integer.parseInt(args[0]),
383                     ((args.length > 1) ? args[1] : null));
384    //            if((args.length < 1) || (args.length > 2))
385    //                { throw new IllegalArgumentException(); }
386                }
387    
388            /**More efficient programmatic constructor.
389             * Takes count as the first (int) argument and
390             * a String to drive the hash or null for a totally random selection.
391             */
392            public sortRandomN(final int count, final String hashSrc)
393                {
394                if(count <= 0) { throw new IllegalArgumentException(); }
395                n = count;
396                seed = hashSrc;
397                }
398    
399            /**The upper bound on result size. */
400            private final int n;
401            /**The seed; if null we use a non-repeatable source. */
402            private final String seed;
403    
404            public ExhibitFull[] sort(final AllExhibitProperties aep, final Name.ExhibitFull[] exhibitNames)
405                {
406                // Set up our random-number source.
407                final Random rnd = (seed == null) ?
408                    Rnd.fastRnd : (new Random(seed.hashCode()));
409    
410                Name.ExhibitFull result[];
411                if(exhibitNames.length <= n)
412                    {
413                    // We don't need to discard any of our input set...
414                    result = exhibitNames;
415                    }
416                else
417                    {
418                    // We need to pick n items at random
419                    // from our input set, with no duplicates.
420                    // We'll make a boolean array logically beside
421                    // our input array, and set a slot true when
422                    // we've used the input value at the same index...
423                    final boolean used[] = new boolean[exhibitNames.length];
424    
425                    // Output array...
426                    result = new Name.ExhibitFull[n];
427    
428                    // This will work badly where n is large and just
429                    // smaller than the number of available exhibits.
430                    // We should possibly reverse the mechanism in
431                    // this case, and mark values not to use...
432                    for(int i = n; --i >= 0; )
433                        {
434                        // Go on looking for an input slot that we have not yet found...
435                        for( ; ; )
436                            {
437                            // Pick an input slot at random...
438                            final int slot = rnd.nextInt(exhibitNames.length);
439                            // If we've already used the value in this slot, try again...
440                            if(used[slot]) { continue; }
441                            // Whopee!  A new one; capture this one and break out...
442                            result[i] = exhibitNames[slot];
443                            used[slot] = true;
444                            break;
445                            }
446                        }
447                    }
448    
449                // Randomise the result set...
450                Collections.shuffle(Arrays.asList(result), rnd);
451    
452                return(result);
453                }
454    
455            /**Equality depends on identical seeds and result size.
456             * In other words this checks equivalence rather than the
457             * results being identical each time.
458             */
459            @Override
460            public boolean equals(final Object obj)
461                {
462                if(!(obj instanceof sortRandomN)) { return(false); }
463                final sortRandomN other = (sortRandomN) obj;
464    
465                if(n != other.n) { return(false); }
466    
467                // If exactly the same seed (eg a static final), then equal.
468                if(seed == other.seed) { return(true); }
469    
470                // If we are null (and therefore the other is not), then not equal.
471                if(seed == null) { return(false); }
472    
473                // Check seeds for equality.
474                return(seed.equals(other.seed));
475                }
476    
477            /**Hash depends on the seed and the result size limit. */
478            @Override
479            public int hashCode()
480                {
481                return(n + ((seed == null) ? 0 : seed.hashCode()));
482                }
483            }
484    
485    
486        /**Sorts exhibits by (ascending) timestamp, ie oldest first.
487         * Takes no parameters, so all instances of this class are equal.
488         */
489        public   static final class sortByTimestamp implements SortIF
490            {
491            /**Unique serial ID. */
492            private static final long serialVersionUID = -8775508119553092181L;
493    
494            /**Ignores its arguments.
495             */
496            public   sortByTimestamp(final String args[])
497                {  }
498    
499            /**Programmatic constructor.
500             */
501            public   sortByTimestamp()
502                {  }
503    
504            /**Sorts its arguments in situ by ascending timestamp (oldest first).
505             * Ties are broken by full name.
506             */
507            public ExhibitFull[] sort(final AllExhibitProperties aep,
508                                 final Name.ExhibitFull[] exhibitNames)
509                {
510                // Get all ESA values in one go, sort in place, then put back the names.
511                final int length = exhibitNames.length;
512                final ExhibitStaticAttr[] esas = new ExhibitStaticAttr[length];
513                for(int i = length; --i >= 0; )
514                    { esas[i] = aep.aeid.getStaticAttr(exhibitNames[i]); }
515    
516                Arrays.sort(esas,
517                    new Comparator<ExhibitStaticAttr>(){
518                        public final int compare(final ExhibitStaticAttr e1, final ExhibitStaticAttr e2)
519                            {
520                            if(e1.timestamp < e2.timestamp) { return(-1); } // Correct.
521                            if(e1.timestamp > e2.timestamp) { return(+1); } // Wrong.
522                            // Break ties unconditionally by name...
523                            return(e1.compareTo(e2));
524                            }
525                        });
526    
527                // Copy back the names...
528                for(int i = length; --i >= 0; )
529                    { exhibitNames[i] = esas[i].getExhibitFullName(); }
530    
531                return(exhibitNames);
532                }
533    
534            /**All instances of this class are identical.
535             */
536            @Override
537            public boolean equals(final Object obj)
538                {
539                if(!(obj instanceof sortByTimestamp)) { return(false); }
540                return(true);
541                }
542    
543            /**Hash is constant. */
544            @Override
545            public int hashCode()
546                {
547                return(0);
548                }
549            }
550    
551    
552        /**Sorts exhibits by (descending) goodness, ie best first.
553         * Takes no parameters, so all instances of this class are equal.
554         * <p>
555         * Cannot supply all the data needed to recompute a full version,
556         * so works with what has already been computed or may force an
557         * approximate ("fast") computation.
558         * <p>
559         * This permits 'stale' values since forcing full recomputation may be very slow
560         * or simply be impossible for some exhibits.
561         */
562        public   static final class sortByGoodness implements SortIF
563            {
564            /**Unique serial ID. */
565            private static final long serialVersionUID = -5596318067630417407L;
566    
567            /**Ignores its arguments.
568             */
569            public   sortByGoodness(final String args[])
570                {
571                nMax = 0;
572                }
573    
574            /**Programmatic constructor.
575             * @param nMax  if positive, is the maximum number of exhibits returned
576             */
577            public   sortByGoodness(final int nMax)
578                {
579                this.nMax = nMax;
580                }
581    
582            /**If positive, is the maximum number of exhibits returned. */
583            private final int nMax;
584    
585            /**Minimum number of values worth attempting to filter in parallel; strictly positive.
586             * This plays off parallelism overhead vs available concurrency
587             * but should not be especially critical an order of magnitude either way.
588             */
589            private static final int MIN_SIZE_FOR_CONCURRENCY = 256;
590    
591            /**Sorts its arguments in situ by descending goodness.
592             * Ties are broken by full name.
593             */
594            public ExhibitFull[] sort(final AllExhibitProperties aep,
595                                 final Name.ExhibitFull[] exhibitNames)
596                {
597                // Dummy/default for quick EPCM approximation.
598                final GenProps gp = new GenProps();
599    
600                // Compute/fetch EPCM (int) goodness *once* for each entry, possibly in parallel.
601                final int length = exhibitNames.length;
602    
603                // Don't attempt to parallelise for small data sets nor on uni-processor JVMs.
604                // Assume that computing (fast-approximation) EPCM values is mainly CPU-bound.
605                final boolean parallelise = ((ThreadUtils.AVAILABLE_PROCESSORS > 1) && (length > MIN_SIZE_FOR_CONCURRENCY));
606    
607                final Map<Name.ExhibitFull,Integer> goodness = parallelise ?
608                    new ConcurrentHashMap<Name.ExhibitFull,Integer>(length, 0.5f, 2*ThreadUtils.AVAILABLE_PROCESSORS) /* Safe to post updates concurrently. */ :
609                    new HashMap<Name.ExhibitFull,Integer>(length);
610    
611                if(!parallelise)
612                    {
613                    // Simple single-threaded method.
614                    for(final Name.ExhibitFull name : exhibitNames)
615                        { goodness.put(name, aep.getExhibitPropsComputableMutable(name, true, gp, null, null).getGoodness()); }
616                    }
617                else
618                    {
619                    // Have the number of parallel tasks go up slowly (with the log of the work size)
620                    // and capped by the number of available CPUs.
621                    final int nTasks = Math.min(ThreadUtils.AVAILABLE_PROCESSORS, GenUtils.log2Approx(length));
622                    assert(nTasks > 1);
623                    assert(nTasks < length);
624    
625                    // Size of each partition: one partition/task will have any extra residue.
626                    final int partitionSize = length / nTasks;
627                    assert(partitionSize > 0);
628    
629                    // Create the tasks.
630                    final Collection<Callable<Object>> tasks = new ArrayList<Callable<Object>>(nTasks);
631                    int taskEnd = length;
632                    for(int i = nTasks; --i >= 0; taskEnd -= partitionSize)
633                        {
634                        // Note that task 0 may be slightly larger than the other tasks.
635                        final int end = taskEnd; // Exclusive end in array.
636                        final int start = (i == 0) ? 0 : (taskEnd - partitionSize); // Inclusive start in array.
637                        tasks.add(new Callable<Object>(){
638                            public Object call()
639                                {
640                                // Directly accumulate results in the thread-safe map.
641                                for(int i = end; --i >= start; )
642                                    {
643                                    final Name.ExhibitFull name = exhibitNames[i];
644                                    goodness.put(name, aep.getExhibitPropsComputableMutable(name, true, gp, null, null).getGoodness());
645                                    }
646                                return(null);
647                                }
648                            });
649                        }
650                    // Execute them all, as concurrently as possible.
651                    try { ThreadUtils.computeIntensiveThreadPool.invokeAll(tasks); }
652                    // If we were interrupted or an exception was thrown, re-throw here.
653                    catch(final Exception e) { throw new RuntimeException("unexpected exception", e); }
654                    }
655    
656                // TODO: replace this with a heap of the first nMax items for efficiency.
657                Arrays.sort(exhibitNames,
658                    new Comparator<Name.ExhibitFull>(){
659                        public final int compare(final Name.ExhibitFull n1, final Name.ExhibitFull n2)
660                            {
661                            // Sort best items first.
662                            final int g1 = goodness.get(n1);
663                            final int g2 = goodness.get(n2);
664                            if(g1 > g2) { return(-1); } // Correct.
665                            if(g1 < g2) { return(+1); } // Wrong.
666    
667                            // Break ties (not resolved by goodness).
668                            // Sort newer items first
669                            // as potentially more interesting
670                            // and so that they will get looked at!
671                            final ExhibitStaticAttr e1 = aep.aeid.getStaticAttr(n1);
672                            final ExhibitStaticAttr e2 = aep.aeid.getStaticAttr(n2);
673                            if(e1.timestamp > e2.timestamp) { return(-1); }
674                            if(e1.timestamp < e2.timestamp) { return(+1); }
675    
676                            // Break tie unconditionally by name.
677                            // Should almost never be used.
678                            // Only here to guarantee a total ordering.
679                            return(TextUtils.compare(e1.getCharSequence(), e2.getCharSequence()));
680                            }
681                        });
682    
683                // If we are limiting the result length,
684                // then return the "creme de la creme" here!
685                if((nMax > 0) && (nMax < length))
686                    {
687                    // Return a subset of the items; the best/first ones in fact...
688                    return(Arrays.copyOf(exhibitNames, nMax));
689                    }
690    
691                return(exhibitNames);
692                }
693    
694            /**All instances of this class are identical where their nMax is.
695             */
696            @Override
697            public boolean equals(final Object obj)
698                {
699                if(!(obj instanceof sortByGoodness)) { return(false); }
700                final sortByGoodness other = (sortByGoodness) obj;
701                return(nMax == other.nMax);
702                }
703    
704            /**Hash is constant. */
705            @Override
706            public int hashCode()
707                {
708                return(nMax);
709                }
710            }
711    
712        /**Simple sort of exhibits by name.
713         * By default sorts in natural String order,
714         * but can take a Comparator argument to use
715         * (eg to use a 'smart' ordering(s) for human benefit).
716         */
717        public   static final class sortByName implements SortIF
718            {
719            /**Unique serial ID. */
720            private static final long serialVersionUID = 9042557392754106067L;
721    
722            /**Simple sort; default String lexical ordering; can use null instead. */
723            public static final String SIMPLE = "simple";
724    
725            /**Simple smart sort; essentially case-insensitive ordering on file component. */
726            public static final String SIMPLE_SMART = "simpleSmart";
727    
728            /**Full smart sort; gets current version of comparator during sort().
729             * This uses the data-dependent sort
730             * (dependent on the set of attribute words)
731             * that comes with the data so that it is always current.
732             */
733            public static final String SMART = "smart";
734    
735            /**Sort Comparator; null for default (simple) sort. */
736            private final String sortType;
737    
738            /**Call with argument giving sort type (or null for natural ordering).
739             * Recognised values are:
740             * <ul>
741             * <li>simple (default String lexical ordering)
742             * <li>simpleSmart (essentially case-insensitive ordering on file component)
743             * <li>smart (fully-smart case-insensitive sort using attribute words)
744             * </ul>
745             */
746            public sortByName(final String sortType)
747                {
748                this.sortType = sortType;
749                }
750    
751    
752            /**Call with no args or single argument giving sort type.
753             * Recognised values are:
754             * <ul>
755             * <li>simple (default String lexical ordering)
756             * <li>simpleSmart (essentially case-insensitive ordering on file component)
757             * <li>smart (fully-smart case-insensitive sort using attribute words)
758             * </ul>
759             */
760            public sortByName(final String args[])
761                {
762                if((args != null) && (args.length > 0))
763                    {
764                    sortType = args[0];
765                    }
766                else
767                    { sortType = null; } // Default sort.
768                }
769    
770            public ExhibitFull[] sort(final AllExhibitProperties aep, final Name.ExhibitFull[] exhibitNames)
771                {
772                // Simple smart order: not dependent on attribute words.
773                if(sortType.equals(SIMPLE_SMART))
774                    { Arrays.sort(exhibitNames, ExhibitName.SIMPLE_SMART_ORDER); }
775    
776                // Fully-smart sort: dependent on attribute words.
777                else if(sortType.equals(SMART))
778                    { Arrays.sort(exhibitNames, ExhibitAttrUtils.getAttrWords().SMART_ORDER); }
779    
780                // Default case (eg if comp is SIMPLE or null or unknown).
781                // Simple sort in String order: fast but not human-friendly.
782                // Not dependent on attribute words.
783                else
784                    { Arrays.sort(exhibitNames); }
785                return(exhibitNames);
786                }
787    
788            /**The hashcode of the sort, or null for the default sort. */
789            @Override
790            public int hashCode()
791                {
792                return(sortType == null ? 91 : sortType.hashCode());
793                }
794    
795            /**Tests equality between two sortByName objects.
796             * For this to be the case the two comparator objects must
797             * be == (eg both be null), or compare equals().
798             */
799            @Override
800            public boolean equals(final Object obj)
801                {
802    //System.err.println("sortByName.equals() ...");
803    
804                // Always equal to myself.
805                if(obj == this) { return(true); }
806    
807                if(!(obj instanceof sortByName)) { return(false); }
808                final sortByName other = (sortByName) obj;
809    
810                // If exactly the same sort type (eg a static final), then equal.
811                if(other.sortType == sortType) { return(true); }
812    
813                // If type is null (and therefore the other is not), then not equal.
814                if(sortType == null) { return(false); }
815    
816                // Compare sort types for equality.
817                if(!sortType.equals(other.sortType)) { return(false); }
818    
819    //System.err.println("sortByName.equals() = true");
820    
821                return(true); // Equivalent sorts.
822                }
823            }
824    
825        }