001    /*
002    Copyright (c) 1996-2011, Damon Hart-Davis
003    All rights reserved.
004    
005    Redistribution and use in source and binary forms, with or without
006    modification, are permitted provided that the following conditions are
007    met:
008    
009      * Redistributions of source code must retain the above copyright
010        notice, this list of conditions and the following disclaimer.
011    
012      * Redistributions in binary form must reproduce the above copyright
013        notice, this list of conditions and the following disclaimer in the
014        documentation and/or other materials provided with the
015        distribution.
016    
017    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
018    IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
019    TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
020    PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
021    OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
022    SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
023    LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
024    DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
025    THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
026    (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
027    OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
028    */
029    package org.hd.d.pg2k.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         */
139        public static final class filtByCategory implements FilterIF
140            {
141            /**Unique serial ID. */
142            private static final long serialVersionUID = -2337407306687756258L;
143            private final String acceptableCategories[];
144            /**Accepts zero or more categories; an exhibit matching any of them is accepted.
145             */
146            public filtByCategory(final String args[]) { acceptableCategories = args.clone(); }
147            public boolean accept(final AllExhibitProperties aep, final Name.ExhibitFull exhibitName)
148                {
149                final CharSequence category = ExhibitName.getCategoryComponent(exhibitName);
150                for(int i = acceptableCategories.length; --i >= 0; )
151                    { if(TextUtils.contentEquals(category, acceptableCategories[i])) { return(true); } }
152                return(false); // No match.
153                }
154            }
155    
156    
157        /**Accepts an exhibit with the given case-sensitive substring in its full pathname.
158         * This is no respector of syntax, or aliases.
159         * <p>
160         * Not fast and does not look at the data or meta-data.
161         * <p>
162         * As a special case this is publicly visible until expression
163         * parsing is implemented.
164         * <p>
165         * FIXME: add equals() and hashCode().
166         */
167        public   static final class filtSimpleSubstringMatch implements FilterIF
168            {
169            /**Unique serial ID. */
170            private static final long serialVersionUID = -5197565900747314559L;
171            /**Takes exactly one argument; the case-sensitive substring to match.
172             */
173            public   filtSimpleSubstringMatch(final String args[])
174                {
175                if((args.length != 1) /* || (args[0] == null) */)
176                    { throw new IllegalArgumentException(); }
177                subString = args[0];
178                }
179            /**The substring to match. */
180            private final String subString;
181            public boolean accept(final AllExhibitProperties aep, final Name.ExhibitFull exhibitName)
182                { return(exhibitName.toString().indexOf(subString) != -1); } // FIXME: get rid of expensive String conversion
183            }
184    
185        /**Accepts an that has an Estd location within the target area; possibly within the specified area.
186         */
187        public   static final class filtByEstdLocationCentre implements FilterIF
188            {
189            /**Unique serial ID. */
190            private static final long serialVersionUID = 1246204565122236630L;
191            /**Particular location to filter by.
192             * If non-null, item must fall within the area delimited by this
193             * location to be considered.
194             */
195            private final Location.Estd area;
196    
197            /**Takes zero or one arguments; the (optional) area and bounds to filter else any Estd location is acceptable.
198             */
199            public   filtByEstdLocationCentre(final String args[])
200                {
201                if((args != null) || (args.length >= 0))
202                    { throw new Error("NOT IMPLEMENTED"); }
203                area = null;
204                }
205    
206            /**Takes area and bounds to filter for; if null any Estd location is acceptable.
207             */
208            public   filtByEstdLocationCentre(final Location.Estd filterArea)
209                {
210                area = filterArea;
211                }
212    
213            public boolean accept(final AllExhibitProperties aep, final Name.ExhibitFull exhibitName)
214                {
215                final Location.Base l = aep.getLocation(exhibitName);
216                if(!(l instanceof Location.Estd))
217                    { return(false); } // Wrong type or no location info.
218    
219                // Filter on area if so requested.
220                if(area != null)
221                    {
222                    final Location.Estd el = (Location.Estd) l;
223                    if(!area.containsCentre(el))
224                        { return(false); } // Centre not in specified area.
225                    }
226    
227                return(true); // Seems OK.
228                }
229    
230            /**Filters are equal if area objects are (or are both null). */
231            @Override
232            public boolean equals(final Object obj)
233                {
234                if(!(obj instanceof filtByEstdLocationCentre)) { return(false); }
235                final filtByEstdLocationCentre other = (filtByEstdLocationCentre) obj;
236                if(area == null) { return(other.area == null); }
237                return(area.equals(other.area));
238                }
239    
240            /**Filter has hashcode of its area; 0 if none. */
241            @Override
242            public int hashCode()
243                {
244                if(area == null) { return(0); }
245                return(area.hashCode());
246                }
247            }
248    
249    
250    
251        /**Accepts an exhibit with the given case-sensitive substring starting its final (file / short-name) component.
252         * This is not much of a respector of syntax.
253         * <p>
254         * Very fast and does not look at the data or meta-data.
255         * <p>
256         * As a special case this is publicly visible until expression
257         * parsing is implemented.
258         * <p>
259         * FIXME: add equals() and hashCode().
260         */
261        public   static final class filtPrefixMatch implements FilterIF
262            {
263            /**Unique serial ID. */
264            private static final long serialVersionUID = 8580496714859968839L;
265            /**Takes exactly one argument; the case-sensitive substring to match. */
266            public   filtPrefixMatch(final String args[])
267                {
268                if((args.length != 1) /* || (args[0] == null) */)
269                    { throw new IllegalArgumentException(); }
270                prefix = args[0];
271                }
272            /**The substring to match. */
273            private final String prefix;
274            public boolean accept(final AllExhibitProperties aep, final Name.ExhibitFull exhibitName)
275                {
276                final ExhibitShort shortName = exhibitName.getShortName();
277                return(TextUtils.startsWith(shortName, prefix));
278    //            return(shortName.toString().startsWith(prefix));
279                }
280            }
281    
282        /**Accepts an exhibit with the given case-sensitive substring ending its final component.
283         * This is not much of a respector of syntax.
284         * <p>
285         * Very fast and does not look at the data or meta-data.
286         * <p>
287         * As a special case this is publicly visible until expression
288         * parsing is implemented.
289         * <p>
290         * FIXME: add equals() and hashCode().
291         */
292        public   static final class filtSimpleSuffixMatch implements FilterIF
293            {
294            /**Unique serial ID. */
295            private static final long serialVersionUID = -6968644372319333517L;
296            /**Takes exactly one argument; the case-sensitive substring to match.
297             */
298            public   filtSimpleSuffixMatch(final String args[])
299                {
300                if((args.length != 1) /* || (args[0] == null) */)
301                    { throw new IllegalArgumentException(); }
302                suffix = args[0];
303                }
304            /**The substring to match. */
305            private final String suffix;
306            public boolean accept(final AllExhibitProperties aep, final Name.ExhibitFull exhibitName)
307                {
308                return(TextUtils.endsWith(exhibitName, suffix));
309    //            return(ExhibitName.getFileComponent(exhibitName).toString().endsWith(suffix));
310                }
311            }
312    
313    
314        /**Reverses the existing order of exhibits in situ.
315         * Takes no parameters, so all instances of this class are equal.
316         */
317        public   static final class sortReverse implements SortIF
318            {
319            /**Unique ID. */
320            private static final long serialVersionUID = -3097396087855039990L;
321            /**Ignores its arguments. */
322            public   sortReverse(final String args[]) { this(); }
323            /**Programmatic constructor. */
324            public   sortReverse() { }
325            /**All instances of this class are identical. */
326            @Override
327            public boolean equals(final Object obj) { return((obj instanceof sortReverse)); }
328            /**Hash is constant. */
329            @Override
330            public int hashCode() { return(-99); }
331    
332            /**Reverses the order of its arguments in situ. */
333            public ExhibitFull[] sort(final AllExhibitProperties aep,
334                                 final Name.ExhibitFull[] exhibitNames)
335                {
336                Collections.reverse(Arrays.asList(exhibitNames));
337                return(exhibitNames);
338                }
339            }
340    
341        /**Selects N exhibits at random and randomises their order.
342         * If the number of exhibits is less than N then this returns
343         * all the exhibits.
344         * <p>
345         * The first value is the unsigned, positive, base-ten integer value
346         * of N.
347         * <p>
348         * If the second argument is missing or null then the random number
349         * sequence used is taken differently each time we run, else the
350         * hash of the second string is taken as the random number seed.
351         */
352        public   static final class sortRandomN implements SortIF
353            {
354            /**Unique serial ID. */
355            private static final long serialVersionUID = 2078921830149773203L;
356    
357            /**Takes exactly one or two arguments.
358             * The first argument is the strictly-positive N.
359             * <p>
360             * The second argument, if absent or null, indicates that
361             * a different random number sequence is used on each run.
362             * If present and non-null then its hash is used to seed a
363             * random number generator to give repeatable results.
364             */
365            public   sortRandomN(final String args[])
366                {
367                this(Integer.parseInt(args[0]),
368                     ((args.length > 1) ? args[1] : null));
369    //            if((args.length < 1) || (args.length > 2))
370    //                { throw new IllegalArgumentException(); }
371                }
372    
373            /**More efficient programmatic constructor.
374             * Takes count as the first (int) argument and
375             * a String to drive the hash or null for a totally random selection.
376             */
377            public sortRandomN(final int count, final String hashSrc)
378                {
379                if(count <= 0) { throw new IllegalArgumentException(); }
380                n = count;
381                seed = hashSrc;
382                }
383    
384            /**The upper bound on result size. */
385            private final int n;
386            /**The seed; if null we use a non-repeatable source. */
387            private final String seed;
388    
389            public ExhibitFull[] sort(final AllExhibitProperties aep, final Name.ExhibitFull[] exhibitNames)
390                {
391                // Set up our random-number source.
392                final Random rnd = (seed == null) ?
393                    Rnd.fastRnd : (new Random(seed.hashCode()));
394    
395                Name.ExhibitFull result[];
396                if(exhibitNames.length <= n)
397                    {
398                    // We don't need to discard any of our input set...
399                    result = exhibitNames;
400                    }
401                else
402                    {
403                    // We need to pick n items at random
404                    // from our input set, with no duplicates.
405                    // We'll make a boolean array logically beside
406                    // our input array, and set a slot true when
407                    // we've used the input value at the same index...
408                    final boolean used[] = new boolean[exhibitNames.length];
409    
410                    // Output array...
411                    result = new Name.ExhibitFull[n];
412    
413                    // This will work badly where n is large and just
414                    // smaller than the number of available exhibits.
415                    // We should possibly reverse the mechanism in
416                    // this case, and mark values not to use...
417                    for(int i = n; --i >= 0; )
418                        {
419                        // Go on looking for an input slot that we have not yet found...
420                        for( ; ; )
421                            {
422                            // Pick an input slot at random...
423                            final int slot = rnd.nextInt(exhibitNames.length);
424                            // If we've already used the value in this slot, try again...
425                            if(used[slot]) { continue; }
426                            // Whopee!  A new one; capture this one and break out...
427                            result[i] = exhibitNames[slot];
428                            used[slot] = true;
429                            break;
430                            }
431                        }
432                    }
433    
434                // Randomise the result set...
435                Collections.shuffle(Arrays.asList(result), rnd);
436    
437                return(result);
438                }
439    
440            /**Equality depends on identical seeds and result size.
441             * In other words this checks equivalence rather than the
442             * results being identical each time.
443             */
444            @Override
445            public boolean equals(final Object obj)
446                {
447                if(!(obj instanceof sortRandomN)) { return(false); }
448                final sortRandomN other = (sortRandomN) obj;
449    
450                if(n != other.n) { return(false); }
451    
452                // If exactly the same seed (eg a static final), then equal.
453                if(seed == other.seed) { return(true); }
454    
455                // If we are null (and therefore the other is not), then not equal.
456                if(seed == null) { return(false); }
457    
458                // Check seeds for equality.
459                return(seed.equals(other.seed));
460                }
461    
462            /**Hash depends on the seed and the result size limit. */
463            @Override
464            public int hashCode()
465                {
466                return(n + ((seed == null) ? 0 : seed.hashCode()));
467                }
468            }
469    
470    
471        /**Sorts exhibits by (ascending) timestamp, ie oldest first.
472         * Takes no parameters, so all instances of this class are equal.
473         */
474        public   static final class sortByTimestamp implements SortIF
475            {
476            /**Unique serial ID. */
477            private static final long serialVersionUID = -8775508119553092181L;
478    
479            /**Ignores its arguments.
480             */
481            public   sortByTimestamp(final String args[])
482                {  }
483    
484            /**Programmatic constructor.
485             */
486            public   sortByTimestamp()
487                {  }
488    
489            /**Sorts its arguments in situ by ascending timestamp (oldest first).
490             * Ties are broken by full name.
491             */
492            public ExhibitFull[] sort(final AllExhibitProperties aep,
493                                 final Name.ExhibitFull[] exhibitNames)
494                {
495                // Get all ESA values in one go, sort in place, then put back the names.
496                final int length = exhibitNames.length;
497                final ExhibitStaticAttr[] esas = new ExhibitStaticAttr[length];
498                for(int i = length; --i >= 0; )
499                    { esas[i] = aep.aeid.getStaticAttr(exhibitNames[i]); }
500    
501                Arrays.sort(esas,
502                    new Comparator<ExhibitStaticAttr>(){
503                        public final int compare(final ExhibitStaticAttr e1, final ExhibitStaticAttr e2)
504                            {
505                            if(e1.timestamp < e2.timestamp) { return(-1); } // Correct.
506                            if(e1.timestamp > e2.timestamp) { return(+1); } // Wrong.
507                            // Break ties unconditionally by name...
508                            return(e1.compareTo(e2));
509                            }
510                        });
511    
512                // Copy back the names...
513                for(int i = length; --i >= 0; )
514                    { exhibitNames[i] = esas[i].getExhibitFullName(); }
515    
516                return(exhibitNames);
517                }
518    
519            /**All instances of this class are identical.
520             */
521            @Override
522            public boolean equals(final Object obj)
523                {
524                if(!(obj instanceof sortByTimestamp)) { return(false); }
525                return(true);
526                }
527    
528            /**Hash is constant. */
529            @Override
530            public int hashCode()
531                {
532                return(0);
533                }
534            }
535    
536    
537        /**Sorts exhibits by (descending) goodness, ie best first.
538         * Takes no parameters, so all instances of this class are equal.
539         * <p>
540         * Cannot supply all the data needed to recompute a full version,
541         * so works with what has already been computed or may force an
542         * approximate ("fast") computation.
543         * <p>
544         * This permits 'stale' values since forcing full recomputation may be very slow
545         * or simply be impossible for some exhibits.
546         */
547        public   static final class sortByGoodness implements SortIF
548            {
549            /**Unique serial ID. */
550            private static final long serialVersionUID = -5596318067630417407L;
551    
552            /**Ignores its arguments.
553             */
554            public   sortByGoodness(final String args[])
555                {
556                nMax = 0;
557                }
558    
559            /**Programmatic constructor.
560             * @param nMax  if positive, is the maximum number of exhibits returned
561             */
562            public   sortByGoodness(final int nMax)
563                {
564                this.nMax = nMax;
565                }
566    
567            /**If positive, is the maximum number of exhibits returned. */
568            private final int nMax;
569    
570            /**Minimum number of values worth attempting to filter in parallel; strictly positive.
571             * This plays off parallelism overhead vs available concurrency
572             * but should not be especially critical an order of magnitude either way.
573             */
574            private static final int MIN_SIZE_FOR_CONCURRENCY = 256;
575    
576            /**Sorts its arguments in situ by descending goodness.
577             * Ties are broken by full name.
578             */
579            public ExhibitFull[] sort(final AllExhibitProperties aep,
580                                 final Name.ExhibitFull[] exhibitNames)
581                {
582                // Dummy/default for quick EPCM approximation.
583                final GenProps gp = new GenProps();
584    
585                // Compute/fetch EPCM (int) goodness *once* for each entry, possibly in parallel.
586                final int length = exhibitNames.length;
587    
588                // Don't attempt to parallelise for small data sets nor on uni-processor JVMs.
589                // Assume that computing (fast-approximation) EPCM values is mainly CPU-bound.
590                final boolean parallelise = ((ThreadUtils.AVAILABLE_PROCESSORS > 1) && (length > MIN_SIZE_FOR_CONCURRENCY));
591    
592                final Map<Name.ExhibitFull,Integer> goodness = parallelise ?
593                    new ConcurrentHashMap<Name.ExhibitFull,Integer>(length, 0.5f, 2*ThreadUtils.AVAILABLE_PROCESSORS) /* Safe to post updates concurrently. */ :
594                    new HashMap<Name.ExhibitFull,Integer>(length);
595    
596                if(!parallelise)
597                    {
598                    // Simple single-threaded method.
599                    for(final Name.ExhibitFull name : exhibitNames)
600                        { goodness.put(name, aep.getExhibitPropsComputableMutable(name, true, gp, null, null).getGoodness()); }
601                    }
602                else
603                    {
604                    // Have the number of parallel tasks go up slowly (with the log of the work size)
605                    // and capped by the number of available CPUs.
606                    final int nTasks = Math.min(ThreadUtils.AVAILABLE_PROCESSORS, GenUtils.log2Approx(length));
607                    assert(nTasks > 1);
608                    assert(nTasks < length);
609    
610                    // Size of each partition: one partition/task will have any extra residue.
611                    final int partitionSize = length / nTasks;
612                    assert(partitionSize > 0);
613    
614                    // Create the tasks.
615                    final Collection<Callable<Object>> tasks = new ArrayList<Callable<Object>>(nTasks);
616                    int taskEnd = length;
617                    for(int i = nTasks; --i >= 0; taskEnd -= partitionSize)
618                        {
619                        // Note that task 0 may be slightly larger than the other tasks.
620                        final int end = taskEnd; // Exclusive end in array.
621                        final int start = (i == 0) ? 0 : (taskEnd - partitionSize); // Inclusive start in array.
622                        tasks.add(new Callable<Object>(){
623                            public Object call()
624                                {
625                                // Directly accumulate results in the thread-safe map.
626                                for(int i = end; --i >= start; )
627                                    {
628                                    final Name.ExhibitFull name = exhibitNames[i];
629                                    goodness.put(name, aep.getExhibitPropsComputableMutable(name, true, gp, null, null).getGoodness());
630                                    }
631                                return(null);
632                                }
633                            });
634                        }
635                    // Execute them all, as concurrently as possible.
636                    try { ThreadUtils.computeIntensiveThreadPool.invokeAll(tasks); }
637                    // If we were interrupted or an exception was thrown, re-throw here.
638                    catch(final Exception e) { throw new RuntimeException("unexpected exception", e); }
639                    }
640    
641                // TODO: replace this with a heap of the first nMax items for efficiency.
642                Arrays.sort(exhibitNames,
643                    new Comparator<Name.ExhibitFull>(){
644                        public final int compare(final Name.ExhibitFull n1, final Name.ExhibitFull n2)
645                            {
646                            // Sort best items first.
647                            final int g1 = goodness.get(n1);
648                            final int g2 = goodness.get(n2);
649                            if(g1 > g2) { return(-1); } // Correct.
650                            if(g1 < g2) { return(+1); } // Wrong.
651    
652                            // Break ties (not resolved by goodness).
653                            // Sort newer items first
654                            // as potentially more interesting
655                            // and so that they will get looked at!
656                            final ExhibitStaticAttr e1 = aep.aeid.getStaticAttr(n1);
657                            final ExhibitStaticAttr e2 = aep.aeid.getStaticAttr(n2);
658                            if(e1.timestamp > e2.timestamp) { return(-1); }
659                            if(e1.timestamp < e2.timestamp) { return(+1); }
660    
661                            // Break tie unconditionally by name.
662                            // Should almost never be used.
663                            // Only here to guarantee a total ordering.
664                            return(TextUtils.compare(e1.getCharSequence(), e2.getCharSequence()));
665                            }
666                        });
667    
668                // If we are limiting the result length,
669                // then return the "creme de la creme" here!
670                if((nMax > 0) && (nMax < length))
671                    {
672                    // Return a subset of the items; the best/first ones in fact...
673                    return(Arrays.copyOf(exhibitNames, nMax));
674                    }
675    
676                return(exhibitNames);
677                }
678    
679            /**All instances of this class are identical where their nMax is.
680             */
681            @Override
682            public boolean equals(final Object obj)
683                {
684                if(!(obj instanceof sortByGoodness)) { return(false); }
685                final sortByGoodness other = (sortByGoodness) obj;
686                return(nMax == other.nMax);
687                }
688    
689            /**Hash is constant. */
690            @Override
691            public int hashCode()
692                {
693                return(nMax);
694                }
695            }
696    
697        /**Simple sort of exhibits by name.
698         * By default sorts in natural String order,
699         * but can take a Comparator argument to use
700         * (eg to use a 'smart' ordering(s) for human benefit).
701         */
702        public   static final class sortByName implements SortIF
703            {
704            /**Unique serial ID. */
705            private static final long serialVersionUID = 9042557392754106067L;
706    
707            /**Simple sort; default String lexical ordering; can use null instead. */
708            public static final String SIMPLE = "simple";
709    
710            /**Simple smart sort; essentially case-insensitive ordering on file component. */
711            public static final String SIMPLE_SMART = "simpleSmart";
712    
713            /**Full smart sort; gets current version of comparator during sort().
714             * This uses the data-dependent sort
715             * (dependent on the set of attribute words)
716             * that comes with the data so that it is always current.
717             */
718            public static final String SMART = "smart";
719    
720            /**Sort Comparator; null for default (simple) sort. */
721            private final String sortType;
722    
723            /**Call with argument giving sort type (or null for natural ordering).
724             * Recognised values are:
725             * <ul>
726             * <li>simple (default String lexical ordering)
727             * <li>simpleSmart (essentially case-insensitive ordering on file component)
728             * <li>smart (fully-smart case-insensitive sort using attribute words)
729             * </ul>
730             */
731            public sortByName(final String sortType)
732                {
733                this.sortType = sortType;
734                }
735    
736    
737            /**Call with no args or single argument giving sort type.
738             * Recognised values are:
739             * <ul>
740             * <li>simple (default String lexical ordering)
741             * <li>simpleSmart (essentially case-insensitive ordering on file component)
742             * <li>smart (fully-smart case-insensitive sort using attribute words)
743             * </ul>
744             */
745            public sortByName(final String args[])
746                {
747                if((args != null) && (args.length > 0))
748                    {
749                    sortType = args[0];
750                    }
751                else
752                    { sortType = null; } // Default sort.
753                }
754    
755            public ExhibitFull[] sort(final AllExhibitProperties aep, final Name.ExhibitFull[] exhibitNames)
756                {
757                // Simple smart order: not dependent on attribute words.
758                if(sortType.equals(SIMPLE_SMART))
759                    { Arrays.sort(exhibitNames, ExhibitName.SIMPLE_SMART_ORDER); }
760    
761                // Fully-smart sort: dependent on attribute words.
762                else if(sortType.equals(SMART))
763                    { Arrays.sort(exhibitNames, ExhibitAttrUtils.getAttrWords().SMART_ORDER); }
764    
765                // Default case (eg if comp is SIMPLE or null or unknown).
766                // Simple sort in String order: fast but not human-friendly.
767                // Not dependent on attribute words.
768                else
769                    { Arrays.sort(exhibitNames); }
770                return(exhibitNames);
771                }
772    
773            /**The hashcode of the sort, or null for the default sort. */
774            @Override
775            public int hashCode()
776                {
777                return(sortType == null ? 91 : sortType.hashCode());
778                }
779    
780            /**Tests equality between two sortByName objects.
781             * For this to be the case the two comparator objects must
782             * be == (eg both be null), or compare equals().
783             */
784            @Override
785            public boolean equals(final Object obj)
786                {
787    //System.err.println("sortByName.equals() ...");
788    
789                // Always equal to myself.
790                if(obj == this) { return(true); }
791    
792                if(!(obj instanceof sortByName)) { return(false); }
793                final sortByName other = (sortByName) obj;
794    
795                // If exactly the same sort type (eg a static final), then equal.
796                if(other.sortType == sortType) { return(true); }
797    
798                // If type is null (and therefore the other is not), then not equal.
799                if(sortType == null) { return(false); }
800    
801                // Compare sort types for equality.
802                if(!sortType.equals(other.sortType)) { return(false); }
803    
804    //System.err.println("sortByName.equals() = true");
805    
806                return(true); // Equivalent sorts.
807                }
808            }
809    
810        }