Created
March 30, 2026 22:12
-
-
Save lemire/99a81fc77665184000f87d10db75e475 to your computer and use it in GitHub Desktop.
IpCountryLookup - Reads a file of IP addresses and resolves each to a country using the free ip-api.com JSON API (no key required)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import java.io.*; | |
| import java.net.*; | |
| import java.nio.file.*; | |
| import java.util.*; | |
| import java.util.concurrent.*; | |
| import java.util.concurrent.atomic.*; | |
| import java.util.stream.*; | |
| import java.util.regex.*; | |
| /** | |
| * IpCountryLookup - Reads a file of IP addresses and resolves each to a country | |
| * using the free ip-api.com JSON API (no key required, 45 req/min limit). | |
| * | |
| * Usage: java IpCountryLookup <input-file> [output-file] | |
| * | |
| * Input file format: one IP address per line (blank lines and #-comments ignored) | |
| * | |
| * Example input file: | |
| * 8.8.8.8 | |
| * 1.1.1.1 | |
| * 185.220.101.45 | |
| */ | |
| public class IpCountryLookup { | |
| // Matches IPv4 addresses | |
| private static final Pattern IPV4 = | |
| Pattern.compile("^((25[0-5]|2[0-4]\\d|[01]?\\d\\d?)\\.){3}(25[0-5]|2[0-4]\\d|[01]?\\d\\d?)$"); | |
| // ip-api.com free endpoint (HTTP only for free tier) | |
| private static final String API_URL = "http://ip-api.com/json/%s?fields=status,message,country,countryCode,city,isp,query"; | |
| // Rate limit: free tier allows 45 requests/min → ~2000ms between requests to be safe | |
| private static final int RATE_LIMIT_MS = 2000; | |
| private static final AtomicLong nextAllowedTime = new AtomicLong(System.currentTimeMillis()); | |
| public static void main(String[] args) throws Exception { | |
| if (args.length < 1) { | |
| System.err.println("Usage: java IpCountryLookup <input-file> [output-file]"); | |
| System.exit(1); | |
| } | |
| String inputPath = args[0]; | |
| String outputPath = args.length >= 2 ? args[1] : null; | |
| List<String> ips = loadIps(inputPath); | |
| if (ips.isEmpty()) { | |
| System.out.println("No valid IP addresses found in: " + inputPath); | |
| return; | |
| } | |
| System.out.printf("Found %d IP address(es) to look up.%n%n", ips.size()); | |
| List<String> results = new ArrayList<>(); | |
| String header = String.format("%-18s %-5s %-30s %-20s %s", | |
| "IP Address", "Code", "Country", "City", "ISP"); | |
| String divider = "-".repeat(header.length()); | |
| results.add(header); | |
| results.add(divider); | |
| System.out.println(header); | |
| System.out.println(divider); | |
| ExecutorService executor = Executors.newFixedThreadPool(2); // Limit concurrent requests | |
| List<Future<Result>> futures = new ArrayList<>(); | |
| for (int i = 0; i < ips.size(); i++) { | |
| int index = i; | |
| Future<Result> future = executor.submit(() -> { | |
| String ip = ips.get(index); | |
| String line = lookup(ip); | |
| System.out.println(line); // Print as soon as ready | |
| return new Result(index, line); | |
| }); | |
| futures.add(future); | |
| } | |
| executor.shutdown(); | |
| List<Result> resultList = new ArrayList<>(); | |
| for (Future<Result> f : futures) { | |
| try { | |
| resultList.add(f.get()); | |
| } catch (Exception e) { | |
| System.err.println("Error processing: " + e.getMessage()); | |
| } | |
| } | |
| List<String> lookupResults = resultList.stream() | |
| .sorted(Comparator.comparingInt(r -> r.index)) | |
| .map(r -> r.line) | |
| .collect(Collectors.toList()); | |
| results.addAll(lookupResults); | |
| Set<String> countries = resultList.stream() | |
| .map(r -> extractCountry(r.line)) | |
| .filter(c -> !c.isEmpty()) | |
| .collect(Collectors.toCollection(TreeSet::new)); | |
| if (outputPath != null) { | |
| Files.write(Paths.get(outputPath), results); | |
| System.out.println("\nResults saved to: " + outputPath); | |
| } | |
| if (!countries.isEmpty()) { | |
| System.out.println("\nCountries found:"); | |
| for (String c : countries) { | |
| System.out.println(c); | |
| } | |
| } | |
| } | |
| /** Read and validate IP addresses from the input file. */ | |
| private static List<String> loadIps(String path) throws IOException { | |
| List<String> valid = new ArrayList<>(); | |
| int lineNum = 0; | |
| for (String raw : Files.readAllLines(Paths.get(path))) { | |
| lineNum++; | |
| String line = raw.trim(); | |
| if (line.isEmpty() || line.startsWith("#")) continue; | |
| if (IPV4.matcher(line).matches()) { | |
| valid.add(line); | |
| } else { | |
| System.err.printf("Line %d skipped (not a valid IPv4): %s%n", lineNum, line); | |
| } | |
| } | |
| return valid; | |
| } | |
| /** Call ip-api.com and format the result row. */ | |
| private static String lookup(String ip) { | |
| // Throttle requests to respect rate limit | |
| long now; | |
| while (true) { | |
| now = System.currentTimeMillis(); | |
| long allowed = nextAllowedTime.get(); | |
| if (now >= allowed) { | |
| if (nextAllowedTime.compareAndSet(allowed, now + RATE_LIMIT_MS)) { | |
| break; | |
| } | |
| } else { | |
| long wait = allowed - now; | |
| try { | |
| Thread.sleep(wait); | |
| } catch (InterruptedException e) { | |
| Thread.currentThread().interrupt(); | |
| return formatRow(ip, "ERR", "Interrupted", "", ""); | |
| } | |
| } | |
| } | |
| try { | |
| URL url = new URL(String.format(API_URL, ip)); | |
| HttpURLConnection conn = (HttpURLConnection) url.openConnection(); | |
| conn.setRequestMethod("GET"); | |
| conn.setConnectTimeout(5000); | |
| conn.setReadTimeout(5000); | |
| conn.setRequestProperty("User-Agent", "IpCountryLookup/1.0"); | |
| int status = conn.getResponseCode(); | |
| if (status != 200) { | |
| return formatRow(ip, "ERR", "HTTP " + status, "", ""); | |
| } | |
| StringBuilder sb = new StringBuilder(); | |
| try (BufferedReader br = new BufferedReader( | |
| new InputStreamReader(conn.getInputStream()))) { | |
| String s; | |
| while ((s = br.readLine()) != null) sb.append(s); | |
| } | |
| String json = sb.toString(); | |
| // Simple field extraction (no external JSON lib needed) | |
| String apiStatus = extractField(json, "status"); | |
| String country = extractField(json, "country"); | |
| String countryCode = extractField(json, "countryCode"); | |
| String city = extractField(json, "city"); | |
| String isp = extractField(json, "isp"); | |
| if (!"success".equals(apiStatus)) { | |
| String msg = extractField(json, "message"); | |
| return formatRow(ip, "??", "FAILED: " + msg, "", ""); | |
| } | |
| return formatRow(ip, countryCode, country, city, isp); | |
| } catch (Exception e) { | |
| return formatRow(ip, "ERR", e.getMessage(), "", ""); | |
| } | |
| } | |
| private static String formatRow(String ip, String code, String country, String city, String isp) { | |
| return String.format("%-18s %-5s %-30s %-20s %s", ip, code, country, city, isp); | |
| } | |
| private static String extractCountry(String line) { | |
| String[] parts = line.trim().split("\\s+", 5); | |
| if (parts.length < 3) return ""; | |
| String code = parts[1]; | |
| if ("??".equals(code) || "ERR".equals(code)) return ""; | |
| return parts[2]; | |
| } | |
| /** Minimal JSON string-field extractor (no dependencies required). */ | |
| private static String extractField(String json, String key) { | |
| String search = "\"" + key + "\""; | |
| int idx = json.indexOf(search); | |
| if (idx == -1) return ""; | |
| int colon = json.indexOf(":", idx + search.length()); | |
| if (colon == -1) return ""; | |
| int start = colon + 1; | |
| while (start < json.length() && (json.charAt(start) == ' ' || json.charAt(start) == '\t')) start++; | |
| if (start >= json.length()) return ""; | |
| char first = json.charAt(start); | |
| if (first == '"') { | |
| int end = json.indexOf('"', start + 1); | |
| return end == -1 ? "" : json.substring(start + 1, end); | |
| } | |
| // Non-string value (number / boolean / null) | |
| int end = start; | |
| while (end < json.length() && ",}".indexOf(json.charAt(end)) == -1) end++; | |
| return json.substring(start, end).trim(); | |
| } | |
| private static class Result { | |
| int index; | |
| String line; | |
| Result(int i, String l) { index = i; line = l; } | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment