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    
030    package org.hd.d.pg2k.svrCore.location;
031    
032    import java.net.InetAddress;
033    import java.util.Collections;
034    import java.util.Enumeration;
035    import java.util.HashMap;
036    import java.util.HashSet;
037    import java.util.Iterator;
038    import java.util.Map;
039    import java.util.MissingResourceException;
040    import java.util.ResourceBundle;
041    import java.util.Set;
042    import java.util.SortedMap;
043    import java.util.StringTokenizer;
044    import java.util.TreeMap;
045    
046    import org.hd.d.pg2k.svrCore.AddrTools;
047    import org.hd.d.pg2k.svrCore.MemoryTools;
048    
049    import ORG.hd.d.IsDebug;
050    
051    
052    /**Geographical-related utility functions.
053     * This contains utilities, for example, to guess the approximate geographical
054     * location of an HTTP client from its IP address.
055     */
056    public final class GeoUtils
057        {
058        /**Prevent construction of an instance. */
059        private GeoUtils() { }
060    
061    
062        /**The base name of the default geographic proximity bundle.
063         * Does not include ".properties" or ".class" suffix,
064         * and is assumed to be relative to the root of the package structure.
065         */
066        private static final String BUNDLE_NAME_DEFAULT_GEO_PROXIMITY = "defaultGeoProximity";
067    
068        /**The base name of the default IP-to-location bundle.
069         * Does not include ".properties" or ".class" suffix,
070         * and is assumed to be relative to the root of the package structure.
071         */
072        private static final String BUNDLE_NAME_DEFAULT_CCTLD_FROM_IP_PREFIX = "ccTLDFromIPPrefix";
073    
074        /**Immutable Map for default proximity from CCTLD to Set of all neighbours.
075         * The values (Set<CCTLD>) are also immutable,
076         * so they can be returned to callers safely without copying.
077         */
078        private static final Map<GeoUtils.CCTLD,Set<GeoUtils.CCTLD>> defaultGeoMap;
079    
080        static
081            {
082            final ResourceBundle dGP; // = null;
083            final Map<CCTLD,Set<CCTLD>> m = new HashMap<CCTLD, Set<CCTLD>>();
084            try
085                {
086                // Get the bundle if present...
087                dGP = ResourceBundle.getBundle(BUNDLE_NAME_DEFAULT_GEO_PROXIMITY);
088    
089                // Expand into the quick-lookup format.
090                for(final Enumeration<String> en = dGP.getKeys(); en.hasMoreElements(); )
091                    {
092                    final String key = en.nextElement();
093                    final StringTokenizer st = new StringTokenizer(dGP.getString(key));
094                    final Set<CCTLD> group = new HashSet<CCTLD>();
095                    while(st.hasMoreTokens())
096                        {
097                        final String cc = st.nextToken();
098                        final CCTLD cctld = new CCTLD(MemoryTools.intern(cc));
099                        group.add(MemoryTools.intern(cctld));
100                        }
101    
102                    // Add whole group to neighbours of all members of that group.
103                    // Keep the value Sets immutable.
104                    for(final Iterator<CCTLD> it = group.iterator(); it.hasNext(); )
105                        {
106                        final CCTLD cctld = it.next();
107                        final Set<CCTLD> s = m.get(cctld);
108                        final Set<CCTLD> newSet = (s == null) ? new HashSet<CCTLD>() : new HashSet<CCTLD>(s);
109                        newSet.addAll(group);
110                        m.put(cctld, Collections.unmodifiableSet(newSet));
111                        }
112                    }
113                }
114            catch(final Exception e)
115                {
116                System.err.println("ERROR: failed to load/parse geo-proximity bundle: " + BUNDLE_NAME_DEFAULT_GEO_PROXIMITY);
117                e.printStackTrace();
118                }
119            finally
120                {
121    //            defaultGeoProximity = dGP;
122                defaultGeoMap = Collections.unmodifiableMap(m);
123                }
124            }
125    
126        /**Get geographically-close ccTLDs to the specified ccTLD; empty if none, but never null.
127         * This logically returns the union of all the groups that
128         * the specified ccTLD is present in,
129         * from static, built-in data, and any loadable data.
130         * <p>
131         * If no near neighbours for the specified ccTLD are known
132         * then an empty Set is returned,
133         * otherwise the specified ccTLD will be present in the result
134         * ie a country is always close to itself.
135         */
136        public static Set<GeoUtils.CCTLD> getCloseCCTLDs(final GeoUtils.CCTLD ccTLD)
137            {
138            final Set<GeoUtils.CCTLD> result = defaultGeoMap.get(ccTLD);
139    
140            // If this is not in group, return an empty set.
141            if(result == null)
142                {
143                final Set<GeoUtils.CCTLD> empty = Collections.emptySet();
144                return(empty);
145                }
146    
147            return(result);
148            }
149    
150        /**Get ccTLDs in the specified region; empty if none, but never null.
151         * This should be a valid registry name
152         * or a name from the "defaultGeoProximity" properties.
153         * <p>
154         * The region name should <em>not</em> be a ccTLD
155         * nor a numeric partial/full address.
156         * <p>
157         * Currently this is computed on the fly each time;
158         * we may need to precompute or cache this result.
159         */
160        public static Set<GeoUtils.CCTLD> getCountriesInRegion(final String region)
161            {
162            // Quick check on argument validity.
163            if((region == null) || (region.length() < 1))
164                { throw new IllegalArgumentException(); }
165    
166            final Set<GeoUtils.CCTLD> result = new HashSet<CCTLD>();
167    
168            try
169                {
170                final ResourceBundle dGP = ResourceBundle.getBundle(BUNDLE_NAME_DEFAULT_GEO_PROXIMITY);
171                final String countries = dGP.getString(region);
172    
173                // Compute the countries list...
174                final StringTokenizer st = new StringTokenizer(countries);
175                while(st.hasMoreTokens())
176                    { result.add(new CCTLD(st.nextToken())); }
177                }
178            catch(final MissingResourceException e)
179                {
180                // Ignore these entirely...
181                }
182            catch(final Exception e)
183                {
184                // Absorb errors, though we don't expect any, so report this...
185                e.printStackTrace();
186                }
187    
188            // Save some heap by returning a fixed empty set if nothing found...
189            if(result.isEmpty())
190                {
191                final Set<GeoUtils.CCTLD> empty = Collections.emptySet();
192                return(empty);
193                }
194    
195            return(result);
196            }
197    
198    
199    
200        /**An immutable DNS ccTLD (two-letter) country code.
201         * Lower-case, two-ASCII-letter code.
202         * <p>
203         * This is <em>not</em> the ISO3316-1 code;
204         * eg the United Kingdom is "uk" not "gb"
205         * though "gb" may be acceptable as an alias.
206         */
207        public static final class CCTLD implements MemoryTools.Internable
208            {
209            /**Construct instance.
210             * Supplied code must not be null
211             * and must consist of exactly two lower-case 7-bit-ASCII letters.
212             * <p>
213             * The code is only checked for syntax,
214             * not that such a country code actually exists.
215             */
216            public CCTLD(final String code)
217                {
218                if(!isSyntaticallyValidCcTLD(code))
219                    { throw new IllegalArgumentException("code must be non-null length-two lower-case-ASCII"); }
220    
221                this.code = code;
222                }
223    
224            /**The two-letter lower-case-ASCII ccTLD; never null. */
225            public final String code;
226    
227            /**True if argument is a (non-null) syntatically-valid ccTLD code.
228             * This only looks for syntactic validity.
229             *
230             * @return true iff two-letter lower-case-ASCII argument;
231             *     false for null or otherwise-invalid arguments
232             */
233            public static boolean isSyntaticallyValidCcTLD(final String code)
234                {
235                if((code == null) ||
236                   (code.length() != 2))
237                    { return(false); }
238    
239                final char c0 = code.charAt(0);
240                if((c0 < 'a') || (c0 > 'z'))
241                    { return(false); }
242                final char c1 = code.charAt(1);
243                if((c1 < 'a') || (c1 > 'z'))
244                    { return(false); }
245    
246                // Looks OK.
247                return(true);
248                }
249    
250            /**A fast hash reflecting approx ~5 bits of information per char. */
251            @Override
252            public int hashCode()
253                { return(code.charAt(0) + 29*code.charAt(1)); }
254    
255            /**Two ccTLDs are the same if their underlying codes are the same. */
256            @Override
257            public boolean equals(final Object obj)
258                {
259                if(obj == this) { return(true); }
260                if(!(obj instanceof CCTLD)) { return(false); }
261                final CCTLD other = (CCTLD) obj;
262                return((code.charAt(0) == other.code.charAt(0)) &&
263                       (code.charAt(1) == other.code.charAt(1)));
264                }
265    
266            /**This returns the code as its String representation. */
267            @Override
268            public String toString()
269                { return(code); }
270            }
271    
272    
273        /**Default maximum prefix octets to look up in registry; strictly positive.
274         * This indicates the most specific lookup that we will attempt.
275         * <p>
276         * We will try the least-specific lookup first anyway,
277         * and just remember the most specific we found, if any,
278         * or failing any match an appropriate (short) numeric prefix instead.
279         * <p>
280         * This may be overridable from the data.
281         */
282        private static final int DEFAULT_MAX_OCTETS_LOOKUP = 3;
283    
284        /**Test if argument is a (non-null) syntactically-valid region/registry code.
285         * This only checks syntactic validity.
286         *
287         * @return true iff three-or-more-ASCII-letter argument (1st upper-case);
288         *     false for null or otherwise-invalid arguments
289         */
290        public static boolean isSyntaticallyValidRegistryName(final String code)
291            {
292            if(code == null) { return(false); }
293            final int l = code.length();
294            if(l < 3) { return(false); }
295    
296            // First letter upper-case; the rest any case:
297            // eg RIPE and AfriNIC.
298            final char c0 = code.charAt(0);
299            if((c0 < 'A') || (c0 > 'Z'))
300                { return(false); }
301    
302            for(int i = l; --i > 0; )
303                {
304                final char c = code.charAt(i);
305                final boolean isLetter =
306                    ((c >= 'A') && (c <= 'Z')) ||
307                    ((c >= 'a') && (c <= 'z'));
308                if(!isLetter)
309                    { return(false); }
310                }
311    
312            // Looks OK.
313            return(true);
314            }
315    
316        /**Routine to validate a putative RHS value for the ccTLDFromIPPrefix table.
317         * A valid RHS (right-hand-side) value is one of:
318         * <ul>
319         * <li>An empty string ("") meaning:
320         *     "make sure that there is no mapping for this value".
321         * <li>A valid country code (two-letter lower-case ASCII).
322         * <li>A valid region code (three-or-more-letter mixed-case ASCII).
323         * </ul>
324         *
325         * @return true  iff the value is valid on the right-hand-side of an entry in
326         *     a ccTLDFromIPPrefix map
327         */
328        private static boolean isValidCcTldFromIPPrefixMapValue(final String v)
329            {
330            if("".equals(v)) { return(true); }
331            if(CCTLD.isSyntaticallyValidCcTLD(v)) { return(true); }
332            if(isSyntaticallyValidRegistryName(v)) { return(true); }
333            return(false); // Not valid.
334            }
335    
336        /**Cached immutable default ccTLD-from-IP-prefix SortedMap, or empty if none.
337         * Loaded at class initialisation.
338         * <p>
339         * This may have non-lossy transformations performed on it
340         * before it is stored.
341         * <p>
342         * The values will have been de-duped (intern()ed) for memory efficiency,
343         * since there are relatively few distinct values.
344         * <p>
345         * The keys are essentially unique and are NOT intern()ed,
346         * since attempting to do so would waste memory and time.
347         */
348        private static final SortedMap<AddrTools.AddrPrefix,String> ccTLDFromIPPrefix;
349    
350        /**Get built-in immutable IPv4-to-ccTLD map; never null. */
351        public static SortedMap<AddrTools.AddrPrefix,String> getCcTLDFromIPPrefix()
352            { return(ccTLDFromIPPrefix); }
353    
354        /**Longest key in octets in ccTLDFromIPPrefix; non-negative.
355         * Can be used to restrict search effort at lookup.
356         */
357        private static final int ccTLDFromIPPrefixLongestKey;
358    
359        /**Initialise ccTLDFromIPPrefix. */
360        static
361            {
362            // Create in a HashMap for lookup/insertion speed...
363            // We guess a reasonable initial working size.
364            final Map<AddrTools.AddrPrefix,String> m = new HashMap<AddrTools.AddrPrefix, String>(250123);
365            int longestKey = 0;
366            try
367                {
368                // Get the bundle if present...
369                final ResourceBundle rb = ResourceBundle.getBundle(BUNDLE_NAME_DEFAULT_CCTLD_FROM_IP_PREFIX);
370    
371                // Maximum number of times to warn about discarded (over-long) prefixes.
372                int maxWarnOverlong = 10;
373    
374                // Set of unique RHS values that we've seen so far
375                // (for de-duping with fewer calls to intern(), which is very slow).
376                final HashMap<String,String> deDupedValues = new HashMap<String,String>(511);
377    
378                // Parse each key/value in turn and add to the map if acceptable.
379                final Enumeration<String> keys = rb.getKeys();
380                while(keys.hasMoreElements())
381                    {
382                    final String key = keys.nextElement();
383                    final AddrTools.AddrPrefix ap;
384                    try { ap = new AddrTools.AddrPrefix(key); }
385                    catch(final IllegalArgumentException e)
386                        {
387                        System.err.println("ERROR: skipped unparsable address prefix `"+key+"' in ccTLD-from-IP-prefix bundle: " + BUNDLE_NAME_DEFAULT_CCTLD_FROM_IP_PREFIX);
388                        continue;
389                        }
390    
391                    final int apLen = ap.length();
392                    if(apLen > DEFAULT_MAX_OCTETS_LOOKUP)
393                        {
394                        if(--maxWarnOverlong >= 0)
395                            { System.err.println("WARNING: skipped too-long address prefix `"+key+"' (max "+DEFAULT_MAX_OCTETS_LOOKUP+") in ccTLD-from-IP-prefix bundle: " + BUNDLE_NAME_DEFAULT_CCTLD_FROM_IP_PREFIX); }
396                        continue;
397                        }
398    
399                    // Get RHS (ccTLD/region/etc) code.
400                    final String value = rb.getString(key);
401    
402                    // Locally remove RHS duplicates,
403                    // intern()ing (and validating) new values once.
404                    String dedupedValue = deDupedValues.get(value);
405                    if(null == dedupedValue)
406                        {
407                        // First time we've seen this RHS value so validate/veto it.
408                        if(!isValidCcTldFromIPPrefixMapValue(value))
409                            {
410                            System.err.println("ERROR: skipped unparsable/illegal ccTLD/region `"+value+"' for key `"+key+"' in ccTLD-from-IP-prefix bundle: " + BUNDLE_NAME_DEFAULT_CCTLD_FROM_IP_PREFIX);
411                            continue;
412                            }
413    
414                        dedupedValue = MemoryTools.intern(value);
415                        deDupedValues.put(dedupedValue, dedupedValue);
416                        }
417    
418                    // Note the longest key to help make lookups more efficient.
419                    if(apLen > longestKey)
420                        { longestKey = apLen; }
421    
422                    // Add valid entry to map...
423                    m.put(ap, dedupedValue);
424    //System.out.println("Map size now: " + m.size());
425                    }
426    
427    //System.out.println("deDupedValues size: " + deDupedValues.size());
428    //System.out.println("Map size: " + m.size());
429                }
430            catch(final Exception e)
431                {
432                System.err.println("ERROR: failed to load/parse ccTLD-from-IP-prefix bundle: " + BUNDLE_NAME_DEFAULT_CCTLD_FROM_IP_PREFIX);
433                e.printStackTrace();
434                }
435            finally
436                {
437                // Store as an unmodifiable SortedMap.
438                ccTLDFromIPPrefix = Collections.unmodifiableSortedMap(new TreeMap<AddrTools.AddrPrefix,String>(m));
439                // And record the longest key actually present in the map.
440                ccTLDFromIPPrefixLongestKey = longestKey;
441                }
442    
443    if(IsDebug.isDebug) { System.out.println("[GeoUtils.ccTLDFromIPPrefix.size() = " + ccTLDFromIPPrefix.size() + ".]"); }
444            }
445    
446        /**Guess geographical "location" of given IP address (usually of an HTTP client).
447         * This attempts to guess the top-level country code corresponding to
448         * the likely physical/geographic location of the machine/interface whose
449         * IP address is passed.
450         * <p>
451         * This may try a number of methods, some of which may require live DNS
452         * or other Internet connectivity, but if it fails, with return null.
453         * The results are not guaranteed to be accurate.
454         * <p>
455         * The code returned, if any, is the lower-case, two-letter,
456         * Internet country code, for the region that the IP is in.
457         * (This is not the ISO3166-1 code; the United Kingdom is "uk" not "gb".)
458         * <p>
459         * (Our implementation here is to get the region,
460         * and then return anything that looks like a country-code, else null.)
461         *
462         * @param addr  the (client) address to look up
463         * @param quick  if true, this will try to minimise time taken
464         *     and resources used, eg it may avoid going to DNS,
465         *     so the answer may be less accurate or null more often
466         */
467        public static CCTLD getCCTLDByAddress(final InetAddress addr,
468                                              final boolean quick)
469            {
470            final String guess = getRegionByAddress(addr, quick);
471    
472            // If this looks like a ccTLD,
473            // then try to return it as one.
474            if(CCTLD.isSyntaticallyValidCcTLD(guess))
475                { return(new CCTLD(guess)); }
476    
477            // Don't know.
478            return(null);
479            }
480    
481        /**Guess country or geographic region of IP address; never returns null nor "".
482         * This attempts to guess the top-level country code (ccTLD) corresponding to
483         * the likely physical/geographic location of the machine/interface whose
484         * IP address is passed.  (A two-letter lower-case country code.)
485         * <p>
486         * Failing that this routine may attempt to return the regional IP registry
487         * (eg RIPE or APNIC) that delegates the address,
488         * which usually indicates the continent from which the IP address hails.
489         * (Starting with an upper-case letter and being more than two characters,
490         * ie guaranteed to look like neither a ccTLD nor an IP-address prefix.)
491         * <p>
492         * Failing that this will return one or more octets of the IP(v4) address.
493         * (Eg looking like "127" or "127.0" or "127.0.0" or "127.0.0.1".)
494         * <p>
495         * The number of different values that this routine can return is finite,
496         * certainly no more than thousands, and all values are human-readable,
497         * so this should be directly usable as a key in stats info,
498         * for example.
499         * <p>
500         * This may try a number of methods, some of which may require live DNS
501         * or other Internet connectivity, but if they fail,
502         * this will return a numeric value.
503         * <p>
504         * The results are not guaranteed to be accurate.
505         *
506         * @param addr  the (client) address to look up; never null
507         * @param quick  if true, this will try to minimise time taken
508         *     and resources used, eg it may avoid going to DNS,
509         *     so the answer may be less accurate or numeric more often
510         *
511         * @return non-null, non-empty, short, human-readable, plain-ASCII String
512         */
513        public static String getRegionByAddress(final InetAddress addr,
514                                                final boolean quick)
515            {
516            if(addr == null)
517                { throw new IllegalArgumentException(); }
518    
519            // Look up the address in our map
520            // for the most-specific information available.
521            //
522            // If the result is a null (and we're restricted to a quick lookup)
523            // then we return the numeric representation of the first octet...
524            String result = lookupAddrInIPToCcTLDMap(addr,
525                                    ccTLDFromIPPrefix, ccTLDFromIPPrefixLongestKey);
526            if(result == null)
527                { result = new AddrTools.AddrPrefix(addr.getAddress(), 1).toPaddedDottedPrefix(); }
528    
529            // If the caller is prepared for us to do a slow (ie more accurate or comprehensive) lookup, AND
530            // if the result so far is NOT a country code, AND
531            // if the address looks like a normal unicast address,
532            // then try doing a reverse lookup on the IP address and examine any resulting domain name
533            // and if the name ends with a ccTLD then we will return that ccTLD.
534            if(!quick &&
535                    !GeoUtils.CCTLD.isSyntaticallyValidCcTLD(result) &&
536                    !addr.isMulticastAddress())
537                {
538                try
539                    {
540                    // Try to do a (reverse) lookup of the client host name
541                    // from its IP address...
542                    // We don't need to verify this name;
543                    // if the client lies then it is probably their problem!
544                    // Note that this may have a trailing "."
545                    // to show that it is an absolute FQFN.
546                    String name = AddrTools.USE_DNSJAVA ? AddrTools.doReverseLookup(addr, false) : addr.getHostName();
547    
548    //System.out.println("[Lookup "+addr.getHostAddress()+" --> "+name+"]");
549    
550                    if(name != null)
551                        {
552                        // Strip off any canonical trailing dot...
553                        if(name.endsWith("."))
554                            { name = name.substring(0, name.length()-1); }
555                        assert !name.endsWith(".") : ("had more than one trailing dot in name: "+name+".");
556    
557                        // Allow for names as short as "x.cc"...
558                        final int nameLen = name.length();
559                        if(nameLen > 3)
560                            {
561                            // If the final component is exactly two characters...
562                            if(name.lastIndexOf('.') == nameLen - 3)
563                                {
564                                // Take the final component and force to lower-case.
565                                final String putativeCCTLD =
566                                    name.substring(nameLen - 2).toLowerCase();
567    
568                                // If it is a valid ccTLD,
569                                // then return it immediately...
570                                if(GeoUtils.CCTLD.isSyntaticallyValidCcTLD(putativeCCTLD))
571                                    { return(putativeCCTLD); }
572                                }
573                            }
574                        }
575                    }
576                catch(final Exception e)
577                    {
578                    e.printStackTrace();
579                    // Simply ignore any lookup errors...
580                    }
581                }
582    
583    
584            // Return special (but non-null) value to indicate a config error
585            // if nothing was set...
586            // This does not look like a valid CC nor region nor numeric prefix.
587            if(result == null)
588                { return("-"); }
589    
590            return(result);
591            }
592    
593        /**Do a lookup in the supplied table for the most-specific available location data for the given key; null if none.
594         * Never uses DNS or other external data.
595         *
596         * @param addr  the address to look up; never null
597         * @param map  the prefix map; never null
598         * @param maxOctetsLookup  the maximum number of octets of prefix to look up; non-negative
599         *
600         * @return  the most-specific value; null if nothing (or a "" value) found
601         */
602        public static String lookupAddrInIPToCcTLDMap(final InetAddress addr,
603                                                      final Map<AddrTools.AddrPrefix,String> map,
604                                                      final int maxOctetsLookup)
605            {
606            String result = null;
607    
608            final byte[] rawAddr = addr.getAddress();
609    
610            // Make an increasingly long nnn[.nnn[.nnn ...]] prefix.
611            // Don't go longer than the longest key actually present in the map
612            // since that will always fail to find anything.
613            for(int octets = 1; octets <= maxOctetsLookup; ++octets)
614                {
615                // Stop extending the prefix when we run out of address!
616                if(octets > rawAddr.length)
617                    { break; }
618    
619                final AddrTools.AddrPrefix addrPrefix = new AddrTools.AddrPrefix(rawAddr, octets);
620    
621                // Attempt to look up the region.
622                // Absorb complaint if prefix not present in data.
623                final String region = map.get(addrPrefix);
624    
625                // If we got an empty value then clear any previous result.
626                if("".equals(region))
627                    { result = null; }
628                // If we got a non-null non-empty result,
629                // then it is our most-specific so far, so keep it.
630                else if(region != null)
631                    { result = region; }
632                }
633    
634            return(result);
635            }
636    
637        /**Compute approximate proximity of one host to a given country.
638         * This is most useful where we know the location of (for example)
639         * a mirror host for sure.
640         * <p>
641         * Returns "NONE" if it cannot compute a proximity
642         * or if the hosts appear to be a long way apart.
643         *
644         * @param host1  IP address of first host; never null
645         * @param host2CC  country of second host; never null
646         * @param quick  if true, do a quick estimate,
647         *     else be prepared to spend some time and CPU cycles to be as accurate as possible
648         *     (for example we might have to to reverse DNS lookups)
649         */
650        public static GeoProximity computeProximityByAddress(final InetAddress host1,
651                                                             final CCTLD host2CC,
652                                                             final boolean quick)
653            {
654            if((host1 == null) || (host2CC == null))
655                { throw new IllegalArgumentException(); }
656    
657            // Look up region/country (if known) for the first host.
658            final String region1 = getRegionByAddress(host1, quick);
659    
660            return(computeProximity(region1, host2CC));
661            }
662    
663        /**Compute approximate proximity of a host in a given region to a given country; never null.
664         * This is most useful where we know the location of (for example)
665         * a mirror host for sure.
666         * <p>
667         * Returns "NONE" if it cannot compute a proximity
668         * or if the hosts appear to be a long way apart.
669         *
670         * @param region1  region or country of first host; never null
671         * @param host2CC  country of second host; never null
672         */
673        public static GeoProximity computeProximity(final String region1,
674                                                    final CCTLD host2CC)
675            {
676            if((host2CC == null) || (region1 == null) || (region1.length() < 1))
677                { throw new IllegalArgumentException(); }
678    
679            // If the first host is known to be in the same country as the second
680            // then we're done.
681            if(region1.equals(host2CC.code))
682                { return(GeoProximity.COUNTRY); }
683    
684            // If the first host is a country
685            // then check if we're in the same group/region.
686            if(CCTLD.isSyntaticallyValidCcTLD(region1))
687                {
688                if(getCloseCCTLDs(host2CC).contains(new CCTLD(region1)))
689                    { return(GeoProximity.COUNTRYGROUP); }
690    
691                return(GeoProximity.NONE); // Cannot determine proximity.
692                }
693    
694            // If region seems not to be a valid region name
695            // then give up now...
696            if(!GeoUtils.isSyntaticallyValidRegistryName(region1))
697                { return(GeoProximity.NONE); /* Cannot determine proximity. */ }
698    
699            // Deal with the same registry/region/continent...
700            if(getCountriesInRegion(region1).contains(host2CC))
701                { return(GeoProximity.CONTINENT); }
702    
703            return(GeoProximity.NONE); // Cannot determine proximity.
704            }
705        }