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