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 }