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.svrCore.location;
030    
031    import java.util.ArrayList;
032    import java.util.Collections;
033    import java.util.Enumeration;
034    import java.util.HashMap;
035    import java.util.Map;
036    import java.util.Properties;
037    import java.util.Set;
038    import java.util.StringTokenizer;
039    
040    import org.hd.d.pg2k.svrCore.ExhibitName;
041    import org.hd.d.pg2k.svrCore.Name;
042    import org.hd.d.pg2k.svrCore.PGException;
043    import org.hd.d.pg2k.svrCore.TextUtils;
044    import org.hd.d.pg2k.svrCore.collections.LRUMapAutoSizeForHitRate;
045    
046    // TODO: optimise serialised form on the wire (and in cache).
047    // TODO: optimise prefix sharing (to minimise memory use) between Names in this DB.
048    
049    /**
050     * Created by IntelliJ IDEA.
051     * User: Damon Hart-Davis
052     * Date: 28-Sep-2003
053     * Time: 13:45:37
054     *
055     * Based on pre-PG2K code from ImageInfoCache.
056     */
057    
058    /**Holds a prefix-based lookup map from name to Location.
059     * Is immutable.
060     * <p>
061     * FIXME: needs proper serialisation support such as read/write/validation.
062     */
063    public final class LocationMap implements java.io.Serializable
064        {
065        /**Are location area prefixes case-sensitive? */
066        private static final boolean LOCAREA_PREFIX_CASE_SENSITIVE = false;
067    
068        /**Location key prefix for properties (containing trailing dot). */
069        private static final String locKeyPrefix = "location.";
070    
071        /**Location alias key prefix for properties (containing trailing dot). */
072        private static final String locAliasKeyPrefix = "localias.";
073    
074        /**Timestamp passed in constructor; never negative.
075         * Guaranteed zero if the map is empty.
076         */
077        public final long timestamp;
078    
079        /**Empty, immutable, zero-timestamp, lookup map. */
080        public LocationMap() { this(new Properties(), 0); }
081    
082    
083        /**Two LocationMaps are equal if the underlying maps are. */
084        @Override
085        public boolean equals(final Object obj)
086            {
087            if(!(obj instanceof LocationMap)) { return(false); }
088            return(mapFromNamePrefixToLocation.equals(
089                ((LocationMap) obj).mapFromNamePrefixToLocation));
090            }
091    
092        /**Hash code is that of the underlying map and zero if empty. */
093        @Override
094        public int hashCode()
095            { return(mapFromNamePrefixToLocation.hashCode()); }
096    
097    
098        /**Constructed from non-null Properties containing auxInfo data. */
099        public LocationMap(final Properties p, final long timestamp)
100            {
101            if((timestamp < 0) || (p == null))
102                { throw new IllegalArgumentException(); }
103    
104            mapFromNamePrefixToLocation = Collections.unmodifiableMap(buildMap(p));
105            this.timestamp = ((mapFromNamePrefixToLocation.isEmpty())) ? 0 : timestamp;
106    
107            // Size lookup cache in proportion to memory already used
108            // and to allow for plenty of negative results to be cached too.
109            // Note that 2*sizeMFNPTL is typically more than is ever used...
110            // We're prepared to discard this entirely under acute memory stress.
111            final int sizeMFNPTL = mapFromNamePrefixToLocation.size();
112            lookupCache = LRUMapAutoSizeForHitRate.<Name, Location.Base>create(0.01f, 0, 11 + 16*sizeMFNPTL, 0.75f, "lookupCache");
113    
114            // Store reverse-lookup parameters if present and valid.
115            final String rls = p.getProperty(PNAME_REVERSE_LOOKUP_SECTION);
116            revLookupSection = (!ExhibitName.validNameInitialComponentSyntax(rls)) ?  null :
117                Name.create(LOCAREA_PREFIX_CASE_SENSITIVE ? rls : rls.toLowerCase());
118            int minWords = -1;
119            try { minWords = Integer.parseInt(p.getProperty(PNAME_REVERSE_LOOKUP_MINWORDS), 10); }
120            catch(final NumberFormatException e) { }
121            revLookupMinWords = minWords;
122    
123            // Prepare the reverse map...
124            final Map<Location.Base,Name> revMap = new HashMap<Location.Base, Name>(sizeMFNPTL);
125            final Set<Map.Entry<Name,Location.Base>> entries = mapFromNamePrefixToLocation.entrySet();
126            for(final Map.Entry<Name,Location.Base> entry : entries)
127                {
128                final Name fwdKey = entry.getKey();
129                // If we have a preferred reverse-lookup section
130                // then discard keys starting with anything else.
131                if((revLookupSection != null) && !TextUtils.startsWith(fwdKey, revLookupSection))
132                    { continue; }
133                final Name extant = revMap.get(entry.getValue());
134                if(extant == null)
135                    {
136                    revMap.put(entry.getValue(), fwdKey);
137                    continue;
138                    }
139    System.err.println("WARNING: LocationMap: duplicate keys: " + extant + " and " + fwdKey);
140                if(extant.length() > fwdKey.length())
141                    {
142                    // Keep the shorter key.
143                    revMap.put(entry.getValue(), fwdKey);
144                    }
145                }
146            mapFromLocationToNamePrefix = Collections.unmodifiableMap(revMap);
147            }
148    
149        /**Immutable Map from Name prefix to Location.Base; never null.
150         * Map from "virtual" exhibit name (prefix) to Location object
151         * so that we can reuse previously generated objects.
152         * The exhibit name key consists of the top-level section/directory
153         * and some leading portion of the name;
154         * no intermediate directories are stored.
155         * <p>
156         * A HashMap is preferred for speed.
157         */
158        private final Map<Name,Location.Base> mapFromNamePrefixToLocation;
159    
160        /**Reverse section used for reverse matches, or null if none. */
161        private final Name revLookupSection;
162    
163        /**Minimum number of words matched for reverse matches, or non-positive if none. */
164        private final int revLookupMinWords;
165    
166        /**Immutable reverse map from Location to prefix (as Name); may remove some duplicates.
167         * Must be reconstructed after deserialisation.
168         */
169        private final transient Map<Location.Base,Name> mapFromLocationToNamePrefix;
170    
171        /**Get immutable map from "virtual" prefix to Location.Base (as Name); never null.
172         * We return the internal immutable map.
173         */
174        public Map<Name,Location.Base> getMapFromNamePrefixToLocation()
175            { return(mapFromNamePrefixToLocation); }
176    
177        /**Get immutable reverse map from Location.Base to "virtual" prefix (as Name); never null.
178         * We return the internal immutable map.
179         * <p>
180         * Note that Location values have to be intern()ed or must compare
181         * exactly for equality for this map to be of use.
182         */
183        public Map<Location.Base,Name> getMapFromLocationToNamePrefix()
184            { return(mapFromLocationToNamePrefix); }
185    
186        /**Property name for section to do reverse lookups in. */
187        public static final String PNAME_REVERSE_LOOKUP_SECTION = "_reverse_match_.section";
188    
189        /**Property name for minimum words to match for a reverse lookup. */
190        public static final String PNAME_REVERSE_LOOKUP_MINWORDS = "_reverse_match_.minWords";
191    
192        /**Build the basic map from prefix (as Name) to Location; never null.
193         * The result may be mutable.
194         *
195         * @param rawProperties  properties to extract the data from; never null
196         */
197        private static Map<Name,Location.Base> buildMap(final Properties rawProperties)
198            {
199            if(rawProperties == null)
200                { throw new IllegalArgumentException(); }
201    
202            // We use a HashMap for lookup speed.
203            final Map<Name,Location.Base> result = new HashMap<Name,Location.Base>(1 + rawProperties.size());
204    
205            // Create a temporary lookup cache from full exhibit name to location.
206            final LRUMapAutoSizeForHitRate<Name, Location.Base> cache = LRUMapAutoSizeForHitRate.<Name, Location.Base>create(0.01f, 1+rawProperties.size(), "LocationMap.buildMap() cache");
207    
208            // Get property names in sorted order
209            // to help generate more efficient internal representation.
210            final ArrayList<String> propertyNames = (ArrayList<String>)Collections.list(rawProperties.propertyNames());
211            Collections.sort(propertyNames);
212    
213            // 'prev' Name key value for attempting to enhance prefix sharing and reduce memory footprint.
214            Name prev = null;
215            Name.ExhibitFull prevEF = null;
216    
217            // First deal with the basic location properties.
218            for(final String key : propertyNames)
219                {
220                // If this is not a location key, ignore it.
221                if(!key.startsWith(locKeyPrefix)) { continue; }
222                // OK, parse it, skipping the prefix.
223                final StringTokenizer st = new StringTokenizer(
224                    key.substring(locKeyPrefix.length()), ".");
225                // Abort if not at least two tokens
226                // (the location and a key for Location to interpret).
227                if(st.countTokens() < 2)
228                    {
229    System.err.println("[WARNING: malformed auxInfo key ``" + key + "'': too few components to be valid.]");
230                    continue;
231                    }
232                // Retrieve the prefix and stop if we already
233                // have an entry for it in result.
234                final String synthLocPrefix = st.nextToken();
235                final Name slKey = Name.create(LOCAREA_PREFIX_CASE_SENSITIVE ?
236                    synthLocPrefix : synthLocPrefix.toLowerCase(),
237                    prev);
238                prev = slKey;
239                if(result.get(slKey) != null) { continue; }
240                // If the embedded key does not end with a '-' (or '/'), complain...
241                if(!synthLocPrefix.endsWith(ExhibitName.WORD_SEPS) &&
242                   !synthLocPrefix.endsWith("/"))
243                    {
244    System.err.println("[WARNING: malformed auxInfo key ``" + key + "'': no trailing '"+ExhibitName.WORD_SEP+"' (or '/') in "+synthLocPrefix+".]");
245                    continue;
246                    }
247                // Now attempt to read the (generic) location and
248                // put it in the result.
249                try {
250                    final Location.Base lb = Location.Base.buildFromProperties(
251                        false, locKeyPrefix + synthLocPrefix + '.', rawProperties);
252                    result.put(slKey, lb);
253                    }
254                catch(final PGException e)
255                    {
256    System.err.println("[WARNING: malformed auxInfo location info for ``" + synthLocPrefix + "'': " + e.getMessage() + ".]");
257                    continue;
258                    }
259                }
260    
261            // Now deal with any location aliases.
262            for(final String key : propertyNames)
263                {
264                // If this is not a location key, ignore it.
265                if(!key.startsWith(locAliasKeyPrefix)) { continue; }
266                // OK, parse it, ignoring the prefix.
267                // If the key does not end with a '-' (or a '/'), complain...
268                if(!key.endsWith(ExhibitName.WORD_SEPS) &&
269                   !key.endsWith("/"))
270                    {
271    System.err.println("[WARNING: malformed auxInfo key ``" + key + "'': no trailing '"+ExhibitName.WORD_SEP+"' (or '/').]");
272                    continue;
273                    }
274                final StringTokenizer st = new StringTokenizer(
275                    key.substring(locAliasKeyPrefix.length()), ".");
276                // Abort if not exactly one token.
277                // (the name to be aliased from).
278                if(st.countTokens() != 1)
279                    {
280    System.err.println("[WARNING: malformed auxInfo key ``" + key + "'': too few components to be valid.]");
281                    continue;
282                    }
283    
284                // Retrieve the prefix and stop if we already
285                // have an entry for it in result.
286                final String synthLocPrefix = st.nextToken();
287                final Name slKey = Name.create(LOCAREA_PREFIX_CASE_SENSITIVE ?
288                    synthLocPrefix : synthLocPrefix.toLowerCase(),
289                    prev);
290                prev = slKey;
291                if(result.get(slKey) != null)
292                    {
293    System.err.println("[WARNING: duplicate location alias key ``" + key + "''.]");
294                    continue;
295                    }
296    
297                // Now attempt to look up the pointed-to location
298                // and file our duplicate key/link to it.
299                final String target = rawProperties.getProperty(key);
300                // If the target does not end with a "-", complain...
301                if(!target.endsWith(ExhibitName.WORD_SEPS))
302                    {
303    System.err.println("[WARNING: malformed auxInfo key target ``" + key + "'': no trailing '"+ExhibitName.WORD_SEP+"'.]");
304                    continue;
305                    }
306                final Name.ExhibitFull synthName;
307                try {
308                    synthName = Name.ExhibitFull.create(
309                        target + (target.endsWith("-") ? "" : "-") +
310                        "DHD.jpg", // Fake suffix to make legal full exhibit name.
311                        prevEF);
312                    prevEF = synthName;
313                    }
314                catch(final IllegalArgumentException e)
315                    {
316    e.printStackTrace();
317    System.err.println("[ERROR: problem with location alias: key ``" + key + "'': " + e.getMessage() + ".]");
318                    continue;
319                    }
320    
321    //System.err.println("[Looking up Location alias key ``" + key + "'' as ``"+ synthName +"''.]");
322    
323                // Do lookup (ignoring effect of attribute words).
324                final Location.Base lb = _locLookup(result,
325                                                    synthName,
326                                                    Collections.<String>emptySet(),
327                                                    null,
328                                                    -1,
329                                                    cache);
330                if(lb == Location.NONE)
331                    {
332    System.err.println("[ERROR: location alias key ``" + key + "'' does not point to a valid location (is dangling).]");
333                    continue;
334                    }
335    
336    //System.err.println("[Valid Location alias key ``" + key + "''.]");
337    
338                // Add duplicate.
339                result.put(slKey, lb);
340                }
341    
342            return(result);
343            }
344    
345    
346    
347        /**Lookup of Location by prefix; never null.
348         * Returns Location.NONE if nothing found, never null.
349         *
350         * @param exhibitName  name of exhibit to look up; not null
351         * @param allAttrWords  attribute words; not null
352         */
353        public Location.Base locLookup(final Name.ExhibitFull exhibitName,
354                                       final Set<String> allAttrWords)
355            {
356            return(_locLookup(mapFromNamePrefixToLocation,
357                               exhibitName,
358                               allAttrWords,
359                               revLookupSection,
360                               revLookupMinWords,
361                               lookupCache));
362            }
363    
364        /**Private LRU lookup cache from full name or prefix to location; never null.
365         * Thread-safe and sized in proportion to the size of our main map,
366         * allowing for lots of negative lookups too.
367         * <p>
368         * Not part of the serialised state of the object.
369         * <p>
370         * Results are filed against keys that are both:
371         * <ul>
372         * <li>Exhibit names, for speed ie without needing any string manipulation.
373         * <li>Normalised full forward/reverse names stripped of attribute words
374         *     and monocased as appropriate.
375         * </ul>
376         */
377        private final transient LRUMapAutoSizeForHitRate<Name,Location.Base> lookupCache;
378    
379        /**Lookup of Location by prefix/suffix in given Map; never null.
380         * Returns Location.NONE if nothing found, never null.
381         *
382         * @param exhibitName  full name of exhibit to look up; not null
383         * @param allAttrWords  set of all attribute words
384         *     (if empty, canonical-key entries are not used); never null
385         * @param cache  writable auto-sizing cache map from full or prefix of exhibit name to location
386         */
387        private static Location.Base _locLookup(final Map<Name,Location.Base> nameToLoc,
388                                                final Name.ExhibitFull exhibitName,
389                                                final Set<String> allAttrWords,
390                                                final Name revLookupSection,
391                                                final int revLookupMinWords,
392                                                final LRUMapAutoSizeForHitRate<Name, Location.Base> cache)
393            {
394            // Try cache lookup on full name for positive and negative results.
395            final Location.Base fromCache = cache.get(exhibitName);
396            if(fromCache != null) { return(fromCache); }
397    
398            // Convert the final portion of the exhibit path
399            // to lower-case for comparison.
400            // Note that we are discarding attributes here.
401            // Note the addition of a trailing '-' to ensure
402            // that we match any prefix that should exactly
403            // match our main text.
404            final CharSequence mainWords = exhibitName.getShortName().getMainWordsComponent(allAttrWords).toString();
405    
406            // We need to be careful to lowercase our collection-name key
407            // if everything else is being lowercased.
408            final String collection = ExhibitName.getCategoryComponent(exhibitName).toString();
409            final String collNameKey = (LOCAREA_PREFIX_CASE_SENSITIVE ?
410                collection : collection.toLowerCase());
411    
412            // Construct the canonical "virtual" name stem.
413            final Name searchKeyFwd = Name.create(collNameKey + '/' +
414                (LOCAREA_PREFIX_CASE_SENSITIVE ? mainWords : mainWords.toString().toLowerCase()) + '-');
415    
416            final Location.Base fc1 = cache.get(searchKeyFwd);
417            // If we have a negative result then we won't try a fwd lookup.
418            final boolean dontTryFwd = Location.NONE.equals(fc1);
419            if(!dontTryFwd)
420                {
421                // If we got a positive response then return it immediately.
422                if(fc1 != null) { return(fc1); }
423    
424                // fullLocKey is the lookup key for the Location data,
425                // or null if we found nothing suitable.
426                Name fullLocKey = null;
427    
428                // We can choose one of two algorithms to search
429                // for a longest match in the locations map:
430                //
431                //  1) Look through all the keys in the database in
432                //     turn (in random order).
433                //
434                //  2) Look for matches for successively shorter
435                //     prefixes of our (main text) name.
436                //
437                // The advantages of (2) are its fixed upper bound
438                // on search time (dependent on the name of the
439                // item to be looked up) and its simplicity.
440    //System.err.println("[Searching directly for location by prefix of " + exhibitName.mainText + "; db.size() = " + mapFromNamePrefixToLocation.size() + ".]");
441    
442                // Search for successively shorter prefixes,
443                // stripping off one trailing word each time round the loop.
444                // The first found, if any, is taken to be the best.
445                for(Name lookupKeyFwd = searchKeyFwd; ; )
446                    {
447    //                assert(lookupKeyFwd.endsWith(ExhibitName.WORD_SEPS));
448                    // Now try for the collection-specific.
449                    final Location.Base loc = nameToLoc.get(lookupKeyFwd);
450                    if(loc != null)
451                        {
452                        // Got it!
453                        fullLocKey = lookupKeyFwd;
454                        break;
455                        }
456    
457                    // Try to find the penultimate word separator
458                    // and retain it at the end of the new shorter key.
459                    final int pl = TextUtils.lastIndexOf(lookupKeyFwd, ExhibitName.WORD_SEP, lookupKeyFwd.length()-2);
460                    if(pl == -1) { break; }
461                    lookupKeyFwd = Name.create(lookupKeyFwd.subSequence(0, pl+1), lookupKeyFwd);
462                    }
463    
464                // If we found a match then return it now.
465                if(fullLocKey != null)
466                    {
467    //System.out.println("[Found location prefix match ``"+fullLocKey+"'' for ``"+exhibitName.mainText+"''.]");
468                    final Location.Base result = nameToLoc.get(fullLocKey);
469                    assert(result != null);
470                    assert(result != Location.NONE);
471                    // Cache result under full name.
472                    cache.put(exhibitName, result);
473                    // Record this positive result against the canonical key.
474                    cache.put(searchKeyFwd, result);
475                    return(result);
476                    }
477                // Else cache a negative result for this canonical forward key.
478                else
479                    { cache.put(searchKeyFwd, Location.NONE); }
480                }
481    
482    
483            // Try reverse lookup...
484            // but not for exhibits already in the target section.
485            // We will only use trailing capitalised words for reverse lookup.
486            if((revLookupMinWords > 0) && (revLookupSection != null) &&
487               !revLookupSection.equals(collNameKey))
488                {
489                // Build the new key in reverse order of main words.
490                final StringBuilder sb = new StringBuilder(mainWords.length());
491                int words = 0;
492                for(final Enumeration wordsEn = ExhibitName.getMainWords(exhibitName, allAttrWords); wordsEn.hasMoreElements(); )
493                    {
494                    final String word = (String) wordsEn.nextElement();
495                    // If this word does not have an initial capital,
496                    // reset the collected component.
497                    // Only trailing capitalised words are used.
498                    final char firstLetter = word.charAt(0);
499                    if((firstLetter < 'A') || (firstLetter > 'Z'))
500                        {
501                        sb.setLength(0);
502                        words = 0;
503                        continue;
504                        }
505    
506                    sb.insert(0, '-');
507                    sb.insert(0, word);
508                    ++words;
509                    }
510                final Name searchKeyRev = Name.create(revLookupSection.toString() + '/' +
511                    (LOCAREA_PREFIX_CASE_SENSITIVE ? sb : sb.toString().toLowerCase()),
512                    revLookupSection); // TODO: find better 'prev' value to share prefix with.
513    
514                // Start off looking for the entire canonical key.
515    //System.out.println("Reverse key is "+searchKeyRev+" from exhibit " + exhibitName);
516    
517                final Location.Base fc2 = cache.get(searchKeyRev);
518                // If we have a negative result then we won't try a fwd lookup.
519                final boolean dontTryRev = Location.NONE.equals(fc2);
520                if(!dontTryRev)
521                    {
522                    // If we got a positive response then return it immediately.
523                    if(fc2 != null) { return(fc2); }
524    
525                    // fullLocKey is the lookup key for the Location data,
526                    // or null if we found nothing suitable.
527                    Name fullLocKey = null;
528    
529                    // Search for successively shorter prefixes,
530                    // removing one whole word at a time...
531                    // The first/longest found, if any, is taken to be the best.
532                    // Stop when there are too few words left
533                    // to meet our minimum-match-length restriction.
534                    for(Name lookupKeyRev = searchKeyRev; words-- >= revLookupMinWords; )
535                        {
536    //                    assert(lookupKeyRev.endsWith(ExhibitName.WORD_SEPS)) : ("Key should end with dash: " + lookupKeyRev);
537                        final Location.Base loc = nameToLoc.get(lookupKeyRev);
538    //System.out.println("Lookup (rev) of "+lookupKeyRev+" yields "+loc+ " (words="+(words+1)+")...");
539                        if(loc != null)
540                            {
541                            // Got it!
542                            fullLocKey = lookupKeyRev;
543    //System.out.println("Reverse lookup found "+lookupKeyRev+" for exhibit " + exhibitName);
544                            break;
545                            }
546    
547                        // Try to find the penultimate word separator
548                        // and retain it at the end of the new shorter key.
549                        final int pl = TextUtils.lastIndexOf(lookupKeyRev, ExhibitName.WORD_SEP, lookupKeyRev.length()-2);
550                        if(pl == -1) { break; }
551                        lookupKeyRev = Name.create(lookupKeyRev.subSequence(0, pl+1), lookupKeyRev);
552                        }
553    
554                    // If we found a match then return it now.
555                    if(fullLocKey != null)
556                        {
557                        final Location.Base result = nameToLoc.get(fullLocKey);
558                        assert(result != null);
559                        assert(result != Location.NONE);
560                        // Cache result under full name.
561                        cache.put(exhibitName, result);
562                        // Record this positive result against the canonical key.
563                        cache.put(searchKeyRev, result);
564                        return(result);
565                        }
566                    // Else cache a negative result for this canonical key.
567                    else
568                        { cache.put(searchKeyRev, Location.NONE); }
569                    }
570                }
571    
572            // Cache negative result under full name.
573            cache.put(exhibitName, Location.NONE);
574            return(Location.NONE);
575            }
576    
577        /**Returns true if the map is completely empty, eg as built by the default constructor.
578         */
579        public boolean isEmpty()
580            { return(mapFromNamePrefixToLocation.isEmpty()); }
581    
582    
583    
584    
585    
586    
587    //    /**Retrieves the built-in LocationMap from properties; never null.
588    //     * This is a backup/fallback, checked in with the code.
589    //     * @return  LocationMap, possibly empty but not null
590    //     */
591    //    private static LocationMap _getBuiltInLocationMap()
592    //        {
593    //        try
594    //            {
595    //            // Retrieve the location-map bundle...
596    //            final ResourceBundle lp = ResourceBundle.getBundle(CoreConsts.LOCDB_PROPS_NAME);
597    //
598    //            // Copy into a simple Peroperties object.
599    //            final Properties p = new Properties();
600    //            final Enumeration<String> en = lp.getKeys();
601    //            while(en.hasMoreElements())
602    //                {
603    //                final String key = en.nextElement();
604    //                p.setProperty(key, lp.getString(key));
605    //                }
606    //
607    //            return(new LocationMap(p, 0));
608    //            }
609    //        catch(final Exception e)
610    //            {
611    //            System.err.println("ERROR: could not load built-in location map (using empty one) from: " + CoreConsts.LOCDB_PROPS_NAME);
612    //            e.printStackTrace();
613    //            return(new LocationMap(new Properties(), 0));
614    //            }
615    //        }
616    //
617    //    /**Built-in, possibly vestigial/fallback-only, location map. */
618    //    private static final LocationMap builtInLocationMap = _getBuiltInLocationMap();
619    //
620    //    /**Get public view of built-in location map instance. */
621    //    public static LocationMap getBuiltInLocationMap()
622    //        { return(builtInLocationMap); }
623    
624    
625    
626    
627    
628        /**Serialisation UID. */
629        private static final long serialVersionUID=-8356330012477285265L;
630        }