File History: src/main/java/com/paymentlink/service/GeoIpService.java

← View file content

File Content at Commit 690c1f6

1 package com.paymentlink.service;
2
3 import com.github.benmanes.caffeine.cache.Cache;
4 import com.github.benmanes.caffeine.cache.Caffeine;
5 import com.maxmind.geoip2.DatabaseReader;
6 import com.maxmind.geoip2.exception.GeoIp2Exception;
7 import com.maxmind.geoip2.model.CountryResponse;
8 import com.paymentlink.model.dto.CountryInfo;
9 import com.paymentlink.model.entity.Region;
10 import com.paymentlink.repository.RegionRepository;
11 import jakarta.annotation.PostConstruct;
12 import jakarta.servlet.http.HttpServletRequest;
13 import org.slf4j.Logger;
14 import org.slf4j.LoggerFactory;
15 import org.springframework.beans.factory.annotation.Value;
16 import org.springframework.core.io.ClassPathResource;
17 import org.springframework.stereotype.Service;
18
19 import java.io.IOException;
20 import java.io.InputStream;
21 import java.net.InetAddress;
22 import java.util.Optional;
23 import java.util.concurrent.TimeUnit;
24
25 @Service
26 public class GeoIpService {
27
28 private static final Logger logger = LoggerFactory.getLogger(GeoIpService.class);
29
30 private DatabaseReader databaseReader;
31 private Cache<String, CountryInfo> ipCache;
32
33 private final RegionRepository regionRepository;
34
35 @Value("${geoip.database.path:classpath:ip-to-country.mmdb}")
36 private String databasePath;
37
38 @Value("${geoip.cache.size:10000}")
39 private int cacheSize;
40
41 @Value("${geoip.cache.ttl:3600000}")
42 private long cacheTtl;
43
44 public GeoIpService(RegionRepository regionRepository) {
45 this.regionRepository = regionRepository;
46 }
47
48 @PostConstruct
49 public void init() {
50 logger.info("Initializing GeoIP Service with database: {}", databasePath);
51
52 try {
53 // Try to load MMDB file from classpath
54 ClassPathResource resource = new ClassPathResource("GeoLite2-Country.mmdb");
55 if (resource.exists()) {
56 InputStream inputStream = resource.getInputStream();
57 databaseReader = new DatabaseReader.Builder(inputStream).build();
58 logger.info("GeoIP database loaded successfully");
59 } else {
60 logger.warn("GeoIP database file not found at: {}", databasePath);
61 logger.warn("GeoIP Service will operate in fallback mode (all requests default to US)");
62 }
63 } catch (Exception e) {
64 logger.error("Failed to load GeoIP database: {} - {}", e.getClass().getSimpleName(), e.getMessage());
65 logger.warn("GeoIP Service will operate in fallback mode (all requests default to US)");
66 logger.info("To fix: Download a MaxMind GeoLite2-Country database from https://dev.maxmind.com/geoip/geolite2-free-geolocation-data");
67 databaseReader = null;
68 }
69
70 // Initialize local cache
71 ipCache = Caffeine.newBuilder()
72 .maximumSize(cacheSize)
73 .expireAfterWrite(cacheTtl, TimeUnit.MILLISECONDS)
74 .build();
75
76 logger.info("GeoIP Service initialized with cache size: {} and TTL: {}ms", cacheSize, cacheTtl);
77 }
78
79 /**
80 * Get country information from IP address
81 */
82 public CountryInfo getCountryFromIp(String ipAddress) {
83 if (ipAddress == null || ipAddress.isEmpty()) {
84 return getDefaultCountryInfo();
85 }
86
87 // If database reader is not available, use fallback
88 if (databaseReader == null) {
89 logger.debug("GeoIP database not available, using fallback for IP: {}", ipAddress);
90 return getDefaultCountryInfo();
91 }
92
93 // Check cache first
94 CountryInfo cached = ipCache.getIfPresent(ipAddress);
95 if (cached != null) {
96 logger.debug("Cache hit for IP: {}", ipAddress);
97 return cached;
98 }
99
100 try {
101 InetAddress inetAddress = InetAddress.getByName(ipAddress);
102 CountryResponse response = databaseReader.country(inetAddress);
103 String countryCode = response.getCountry().getIsoCode();
104
105 CountryInfo countryInfo = buildCountryInfo(countryCode);
106
107 // Cache the result
108 ipCache.put(ipAddress, countryInfo);
109
110 logger.debug("GeoIP lookup for IP: {} -> Country: {}", ipAddress, countryCode);
111 return countryInfo;
112
113 } catch (Exception e) {
114 logger.warn("Failed to lookup IP: {} - {}", ipAddress, e.getMessage());
115 return getDefaultCountryInfo();
116 }
117 }
118
119 /**
120 * Extract IP address from HTTP request
121 */
122 public String extractIpFromRequest(HttpServletRequest request) {
123 // Try X-Real-IP first (commonly set by reverse proxies)
124 String ip = request.getHeader("X-Real-IP");
125
126 if (ip == null || ip.isEmpty()) {
127 // Fallback to X-Forwarded-For
128 ip = request.getHeader("X-Forwarded-For");
129 if (ip != null && ip.contains(",")) {
130 // X-Forwarded-For can contain multiple IPs, take the first one
131 ip = ip.split(",")[0].trim();
132 }
133 }
134
135 if (ip == null || ip.isEmpty()) {
136 // Final fallback to remote address
137 ip = request.getRemoteAddr();
138 }
139
140 logger.debug("Extracted IP from request: {}", ip);
141 return ip;
142 }
143
144 /**
145 * Get country info from HTTP request
146 */
147 public CountryInfo getCountryFromRequest(HttpServletRequest request) {
148 String ip = extractIpFromRequest(request);
149 return getCountryFromIp(ip);
150 }
151
152 /**
153 * Build CountryInfo from country code by looking up Region table
154 */
155 private CountryInfo buildCountryInfo(String countryCode) {
156 Optional<Region> regionOpt = regionRepository.findByCountryCode(countryCode);
157
158 if (regionOpt.isPresent()) {
159 Region region = regionOpt.get();
160 return CountryInfo.builder()
161 .countryCode(region.getCountryCode())
162 .countryName(region.getCountryName())
163 .languageCode(region.getLanguageCode())
164 .currencyCode(region.getCurrencyCode())
165 .enabled(region.getEnabled())
166 .build();
167 } else {
168 // Return basic info if region not in database
169 logger.warn("Country code {} not found in regions table", countryCode);
170 return CountryInfo.builder()
171 .countryCode(countryCode)
172 .countryName(countryCode)
173 .languageCode("en")
174 .currencyCode("USD")
175 .enabled(false)
176 .build();
177 }
178 }
179
180 /**
181 * Get default country info (US) when IP lookup fails
182 */
183 private CountryInfo getDefaultCountryInfo() {
184 return CountryInfo.builder()
185 .countryCode("US")
186 .countryName("United States")
187 .languageCode("en")
188 .currencyCode("USD")
189 .enabled(true)
190 .build();
191 }
192
193 /**
194 * Clear cache (for testing or manual refresh)
195 */
196 public void clearCache() {
197 ipCache.invalidateAll();
198 logger.info("GeoIP cache cleared");
199 }
200 }
201

Commits

Commit Author Date Message File SHA Actions
f0438c2 <f69e50@finnacloud.com> 1766443042 +0300 12/22/2025, 10:37:22 PM increment once more e066491 View
188fc92 <f69e50@finnacloud.com> 1766442998 +0300 12/22/2025, 10:36:38 PM increment e066491 View
4617f76 <f69e50@finnacloud.com> 1766442953 +0300 12/22/2025, 10:35:53 PM rename branch from main to master oops e066491 View
e6d1548 <f69e50@finnacloud.com> 1766442769 +0300 12/22/2025, 10:32:49 PM add initial test workflow file e066491 View
9c24ca4 <f69e50@finnacloud.com> 1766442705 +0300 12/22/2025, 10:31:45 PM add CI configuration and test script for Jenkins build e066491 View
690c1f6 <f69e50@finnacloud.com> 1766368110 +0300 12/22/2025, 1:48:30 AM initialize backend structure with controllers, DTOs, and configuration files e066491 Hide