diff --git a/.gitignore b/.gitignore index 50761a1f..895cb5a7 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ *.ear *.sw? *.classpath +.claude .gh-pages .idea .pmd diff --git a/CHANGELOG.md b/CHANGELOG.md index f182dbbe..f4e0efe6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,20 @@ CHANGELOG field. * The deprecation notices for IP Risk database support have been removed. IP Risk database support will continue to be maintained. +* A new `Anonymizer` record has been added to the `InsightsResponse` model. This + record consolidates anonymizer information including VPN confidence scores, + network last seen dates, and provider names. It includes the following fields: + `confidence`, `isAnonymous`, `isAnonymousVpn`, `isHostingProvider`, + `isPublicProxy`, `isResidentialProxy`, `isTorExitNode`, `networkLastSeen`, and + `providerName`. +* A new `ipRiskSnapshot` field has been added to the `Traits` record. This field + provides a static risk score (ranging from 0.01 to 99) associated with the IP + address. This is available from the GeoIP2 Precision Insights web service. +* The anonymous IP flags in the `Traits` record (`isAnonymous`, `isAnonymousVpn`, + `isHostingProvider`, `isPublicProxy`, `isResidentialProxy`, and `isTorExitNode`) + have been deprecated in favor of using the new `Anonymizer` record in the + `InsightsResponse`. These fields will continue to work but will be removed in + version 6.0.0. * **BREAKING:** The deprecated `WebServiceClient.Builder` methods `connectTimeout(int)`, `readTimeout(int)`, and `proxy(Proxy)` have been removed. Use `connectTimeout(Duration)`, `requestTimeout(Duration)`, and diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..11507a8e --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,406 @@ +# CLAUDE.md - GeoIP2 Java API + +This document contains guidance for Claude (and other AI assistants) when working with the GeoIP2-java codebase. It captures architectural patterns, conventions, and lessons learned to help maintain consistency and quality. + +## Project Overview + +**GeoIP2-java** is MaxMind's official Java client library for: +- **GeoIP2/GeoLite2 Web Services**: Country, City, and Insights endpoints +- **GeoIP2/GeoLite2 Databases**: Local MMDB file reading for various database types (City, Country, ASN, Anonymous IP, ISP, etc.) + +The library provides both web service clients and database readers that return strongly-typed model objects containing geographic, ISP, anonymizer, and other IP-related data. + +**Key Technologies:** +- Java 17+ (using modern Java features like records) +- Jackson for JSON serialization/deserialization +- MaxMind DB reader for binary database files +- Maven for build management +- JUnit 5 for testing +- WireMock for web service testing + +## Code Architecture + +### Package Structure + +``` +com.maxmind.geoip2/ +├── model/ # Response models (CityResponse, InsightsResponse, etc.) +├── record/ # Data records (City, Location, Traits, Anonymizer, etc.) +├── exception/ # Custom exceptions for error handling +├── DatabaseReader # Local MMDB file reader +├── WebServiceClient # HTTP client for MaxMind web services +└── DatabaseProvider/WebServiceProvider interfaces +``` + +### Key Design Patterns + +#### 1. **Java Records for Immutable Data Models** +All model and record classes use Java records for immutability and conciseness: + +```java +public record Anonymizer( + @JsonProperty("confidence") + Integer confidence, + + @JsonProperty("is_anonymous") + boolean isAnonymous, + + // ... more fields +) implements JsonSerializable { + // Compact canonical constructor for defaults + public Anonymizer { + // Set defaults for null values + } +} +``` + +**Key Points:** +- Records provide automatic `equals()`, `hashCode()`, `toString()`, and accessor methods +- Use `@JsonProperty` for JSON field mapping +- Use `@MaxMindDbParameter` for database field mapping +- Implement compact canonical constructors to set defaults for null values + +#### 2. **Alphabetical Parameter Ordering** +Record parameters are **always** ordered alphabetically by field name. This maintains consistency across the codebase: + +```java +public record InsightsResponse( + Anonymizer anonymizer, // A comes first + City city, // C comes next + Continent continent, // C (alphabetically after "city") + // ... etc. +) +``` + +#### 3. **Deprecation Strategy** + +When deprecating fields: + +**For record parameters** (preferred for new deprecations): +```java +public record Traits( + @Deprecated(since = "5.0.0", forRemoval = true) + @JsonProperty("is_anonymous") + boolean isAnonymous, + // ... +) +``` + +This automatically marks the accessor method (`isAnonymous()`) as deprecated. + +**For JavaBeans-style getters** (legacy code only): +```java +@Deprecated(since = "5.0.0", forRemoval = true) +public String getUserType() { + return userType(); +} +``` + +**Do NOT add deprecated getters for new fields** - they're only needed for backward compatibility with existing fields that had JavaBeans-style getters before the record migration. + +#### 4. **Default Constructors for Record Classes** + +All record classes in `src/main/java/com/maxmind/geoip2/record/` should provide a no-arg constructor that sets sensible defaults: + +```java +public Anonymizer() { + this(null, false, false, false, false, false, false, null, null); +} +``` + +- Nullable fields → `null` +- Boolean fields → `false` + +**Note:** Model classes in `src/main/java/com/maxmind/geoip2/model/` do not require default constructors as they are typically constructed from API responses. + +#### 5. **Web Service Only vs Database Records** + +Some record classes are only used by web services and do **not** need MaxMind DB support: + +**Web Service Only Records** (no `@MaxMindDbParameter` or `@MaxMindDbConstructor`): +- Records that are exclusive to web service responses (e.g., `Anonymizer` for Insights API) +- Only need `@JsonProperty` annotations for JSON deserialization +- Simpler implementation without database parsing logic + +**Database-Supported Records** (need `@MaxMindDbParameter` and often `@MaxMindDbConstructor`): +- Records used by both web services and database files (e.g., `Traits`, `Location`, `City`) +- Need both `@JsonProperty` and `@MaxMindDbParameter` annotations +- May need `@MaxMindDbConstructor` for date parsing or other database-specific conversion + +**How to Determine:** +- Check the JavaDoc - does it say "This is only available from the X web service"? +- Look at existing similar records in the `record/` package +- If in doubt, ask - adding unnecessary database support adds complexity + +## Testing Conventions + +### Test Structure + +Tests are organized by model/class: +- `src/test/java/com/maxmind/geoip2/model/` - Response model tests +- `src/test/resources/test-data/` - JSON fixtures for tests + +### JSON Test Fixtures + +When adding new fields to responses: +1. Update the JSON fixture files in `src/test/resources/test-data/` +2. Update the corresponding test methods in `*Test.java` files +3. Update `JsonTest.java` to include the new fields in round-trip tests + +Example: Adding `anonymizer` to `InsightsResponse`: +```json +{ + "anonymizer": { + "confidence": 99, + "is_anonymous": true, + "network_last_seen": "2024-12-31", + "provider_name": "NordVPN" + }, + // ... other fields +} +``` + +### WireMock for Web Service Tests + +Web service tests use WireMock to stub HTTP responses: +```java +wireMock.stubFor(get(urlEqualTo("/geoip/v2.1/insights/1.1.1.1")) + .willReturn(aResponse() + .withStatus(200) + .withBody(readJsonFile("insights0")))); +``` + +## Working with This Codebase + +### Adding New Fields to Existing Records + +1. **Determine alphabetical position** for the new field +2. **Add the field** with proper annotations: + ```java + @JsonProperty("field_name") + @MaxMindDbParameter(name = "field_name") + Type fieldName, + ``` +3. **Update the default constructor** (if in `record/` package) to include the new parameter +4. **For minor version releases**: Add a deprecated constructor matching the old signature to avoid breaking changes (see "Avoiding Breaking Changes in Minor Versions" section) +5. **Add JavaDoc** describing the field +6. **Update test fixtures** with example data +7. **Add test assertions** to verify the field is properly deserialized + +### Adding New Records + +When creating a new record class in `src/main/java/com/maxmind/geoip2/record/`: + +1. **Determine if web service only or database-supported** (see "Web Service Only vs Database Records" section) +2. **Follow the pattern** from existing similar records (e.g., `Location`, `Traits`, or `Anonymizer`) +3. **Alphabetize parameters** by field name +4. **Add appropriate annotations**: + - All records: `@JsonProperty` + - Database-supported only: `@MaxMindDbParameter` and possibly `@MaxMindDbConstructor` +5. **Implement `JsonSerializable`** interface +6. **Add a no-arg default constructor** (see section on Default Constructors) +7. **Don't add deprecated getters** - the record accessors are sufficient +8. **Provide comprehensive JavaDoc** for all parameters + +### Deprecation Guidelines + +When deprecating fields in favor of new structures: + +1. **Use `@Deprecated` on record parameters** (not explicit methods) +2. **Include helpful deprecation messages** in JavaDoc pointing to alternatives +3. **Mark as `forRemoval = true`** with appropriate version +4. **Keep deprecated fields functional** - don't break existing code +5. **Update CHANGELOG.md** with deprecation notices + +Example deprecation message: +```java +* @param isAnonymous This is true if the IP address belongs to any sort of anonymous network. + * This field is deprecated. Please use the anonymizer object from the + * Insights response. +``` + +### CHANGELOG.md Format + +Always update `CHANGELOG.md` for user-facing changes: + +```markdown +## 5.0.0 (unreleased) + +* A new `Anonymizer` record has been added... +* A new `ipRiskSnapshot` field has been added... +* The anonymous IP flags have been deprecated... +* **BREAKING:** Description of breaking changes... +``` + +### Avoiding Breaking Changes in Minor Versions + +When adding a new field to an existing record class during a **minor version release** (e.g., 4.x.0 → 4.y.0), you must maintain backward compatibility for users who may be programmatically constructing these records. + +**The Problem:** Adding a field to a record changes the signature of the canonical constructor, which is a breaking change for existing code that constructs the record directly. + +**The Solution:** Add a deprecated constructor that matches the old signature: + +```java +public record Traits( + // ... existing fields ... + String domain, + + // NEW FIELD added in minor version (inserted in alphabetical position) + Double ipRiskSnapshot, + + String organization +) { + // Updated default constructor with new field + public Traits() { + this(null, null, null); + } + + // Deprecated constructor maintaining old signature for backward compatibility + @Deprecated(since = "4.5.0", forRemoval = true) + public Traits( + String domain, + String organization + // Note: ipRiskSnapshot is NOT in this constructor + ) { + this(domain, null, organization); // New field defaults to null (in alphabetical position) + } +} +``` + +**Key Points:** +- The deprecated constructor **matches the signature before the new field was added** +- It calls the new canonical constructor with `null` (or appropriate default) for the new field +- Mark it `@Deprecated` with `forRemoval = true` for the next major version +- Document this in CHANGELOG.md as a new feature, not a breaking change + +**For Major Versions:** You do NOT need to add the deprecated constructor - breaking changes are expected in major version bumps (e.g., 4.x.0 → 5.0.0). + +### Multi-threaded Safety + +Both `DatabaseReader` and `WebServiceClient` are **thread-safe** and should be reused across requests: +- Create once, share across threads +- Reusing clients enables connection pooling and improves performance +- Document thread-safety in JavaDoc for all client classes + +## Common Pitfalls and Solutions + +### Problem: Breaking Changes in Minor Versions +Adding a new field to a record changes the canonical constructor signature, breaking existing code. + +**Solution**: For minor version releases, add a deprecated constructor that maintains the old signature. See "Avoiding Breaking Changes in Minor Versions" section for details. + +### Problem: Record Constructor Ambiguity +When you have two constructors with similar signatures (e.g., both ending with `String`), you may get "ambiguous constructor" errors. + +**Solution**: Cast `null` parameters to their specific type: +```java +this(null, false, null); // Cast if needed: (TypeName) null +``` + +### Problem: Test Failures After Adding New Fields +After adding new fields to a response model, tests fail with deserialization errors. + +**Solution**: Update **all** related test fixtures: +1. Test JSON files (e.g., `insights0.json`, `insights1.json`) +2. In-line JSON in `JsonTest.java` +3. Test assertions in `*ResponseTest.java` files + +## Development Workflow + +### Running Tests +```bash +mvn clean test # Run all tests +mvn test -Dtest=JsonTest # Run specific test class +mvn test -Dtest=InsightsResponseTest,JsonTest # Multiple tests +``` + +### Code Style +- **Checkstyle** enforces code style (see `checkstyle.xml`) +- Run `mvn checkstyle:check` to verify compliance +- Tests must pass checkstyle to merge + +### Version Requirements +- **Java 17+** required +- Uses modern Java features (records, sealed classes potential) +- Target compatibility should match current LTS Java versions + +## Useful Patterns + +### Pattern: Compact Canonical Constructor +Use compact canonical constructors to set defaults and validate: + +```java +public record InsightsResponse( + Anonymizer anonymizer, + City city, + // ... +) { + public InsightsResponse { + anonymizer = anonymizer != null ? anonymizer : new Anonymizer(); + city = city != null ? city : new City(); + // ... + } +} +``` + +### Pattern: Empty Object Defaults +Return empty objects instead of null for better API ergonomics: + +```java +public City city() { + return city; // Never null due to compact constructor +} +``` + +Users can safely call `response.city().name()` even if city data is absent. + +### Pattern: JsonSerializable Interface +All models implement `JsonSerializable` for consistent JSON output: + +```java +public interface JsonSerializable { + default String toJson() throws IOException { + JsonMapper mapper = JsonMapper.builder() + .disable(MapperFeature.CAN_OVERRIDE_ACCESS_MODIFIERS) + .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .addModule(new JavaTimeModule()) + .addModule(new InetAddressModule()) + .serializationInclusion(JsonInclude.Include.NON_NULL) + .build(); + return mapper.writeValueAsString(this); + } +} +``` + +## Database vs Web Service Architecture + +### Database Reader +- Reads binary MMDB files using `maxmind-db` library +- Methods return `Optional` or throw `AddressNotFoundException` +- Support for multiple database types: City, Country, ASN, Anonymous IP, etc. +- Thread-safe, should be reused + +### Web Service Client +- Uses Java 11+ `HttpClient` for HTTP requests +- Methods throw `GeoIp2Exception` or subclasses on errors +- Supports custom timeouts, locales, and proxy configuration +- Thread-safe, connection pooling via reuse + +## Key Dependencies + +- **maxmind-db**: Binary MMDB database reader +- **jackson-databind**: JSON serialization/deserialization +- **jackson-datatype-jsr310**: Java 8+ date/time support +- **wiremock**: HTTP mocking for tests +- **junit-jupiter**: JUnit 5 testing framework + +## Additional Resources + +- [API Documentation](https://maxmind.github.io/GeoIP2-java/) +- [GeoIP2 Web Services Docs](https://dev.maxmind.com/geoip/docs/web-services) +- [MaxMind DB Format](https://maxmind.github.io/MaxMind-DB/) +- GitHub Issues: https://github.com/maxmind/GeoIP2-java/issues + +--- + +*Last Updated: 2024-11-06* diff --git a/src/main/java/com/maxmind/geoip2/JsonSerializable.java b/src/main/java/com/maxmind/geoip2/JsonSerializable.java index 190553cd..a10a6a88 100644 --- a/src/main/java/com/maxmind/geoip2/JsonSerializable.java +++ b/src/main/java/com/maxmind/geoip2/JsonSerializable.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.json.JsonMapper; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import java.io.IOException; @@ -20,6 +21,7 @@ public interface JsonSerializable { default String toJson() throws IOException { JsonMapper mapper = JsonMapper.builder() .disable(MapperFeature.CAN_OVERRIDE_ACCESS_MODIFIERS) + .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) .addModule(new JavaTimeModule()) .addModule(new InetAddressModule()) .serializationInclusion(JsonInclude.Include.NON_NULL) diff --git a/src/main/java/com/maxmind/geoip2/WebServiceClient.java b/src/main/java/com/maxmind/geoip2/WebServiceClient.java index c48169a9..125a3af9 100644 --- a/src/main/java/com/maxmind/geoip2/WebServiceClient.java +++ b/src/main/java/com/maxmind/geoip2/WebServiceClient.java @@ -5,7 +5,9 @@ import com.fasterxml.jackson.databind.InjectableValues; import com.fasterxml.jackson.databind.MapperFeature; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import com.maxmind.geoip2.exception.AddressNotFoundException; import com.maxmind.geoip2.exception.AuthenticationException; import com.maxmind.geoip2.exception.GeoIp2Exception; @@ -133,6 +135,8 @@ private WebServiceClient(Builder builder) { mapper = JsonMapper.builder() .disable(MapperFeature.CAN_OVERRIDE_ACCESS_MODIFIERS) .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) + .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .addModule(new JavaTimeModule()) .build(); requestTimeout = builder.requestTimeout; diff --git a/src/main/java/com/maxmind/geoip2/model/InsightsResponse.java b/src/main/java/com/maxmind/geoip2/model/InsightsResponse.java index 2358153e..c5e67aa3 100644 --- a/src/main/java/com/maxmind/geoip2/model/InsightsResponse.java +++ b/src/main/java/com/maxmind/geoip2/model/InsightsResponse.java @@ -3,6 +3,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import com.maxmind.geoip2.JsonSerializable; +import com.maxmind.geoip2.record.Anonymizer; import com.maxmind.geoip2.record.City; import com.maxmind.geoip2.record.Continent; import com.maxmind.geoip2.record.Country; @@ -19,6 +20,9 @@ * This class provides a model for the data returned by the Insights web * service. * + * @param anonymizer Anonymizer record for the requested IP address. This contains information + * about whether the IP address belongs to an anonymizing network such as a VPN, + * proxy, or Tor exit node. * @param city City record for the requested IP address. * @param continent Continent record for the requested IP address. * @param country Country record for the requested IP address. This object represents the country @@ -43,6 +47,9 @@ * Services */ public record InsightsResponse( + @JsonProperty("anonymizer") + Anonymizer anonymizer, + @JsonProperty("city") City city, @@ -78,6 +85,7 @@ public record InsightsResponse( * Compact canonical constructor that sets defaults for null values. */ public InsightsResponse { + anonymizer = anonymizer != null ? anonymizer : new Anonymizer(); city = city != null ? city : new City(); continent = continent != null ? continent : new Continent(); country = country != null ? country : new Country(); diff --git a/src/main/java/com/maxmind/geoip2/record/Anonymizer.java b/src/main/java/com/maxmind/geoip2/record/Anonymizer.java new file mode 100644 index 00000000..621201c2 --- /dev/null +++ b/src/main/java/com/maxmind/geoip2/record/Anonymizer.java @@ -0,0 +1,71 @@ +package com.maxmind.geoip2.record; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.maxmind.geoip2.JsonSerializable; +import java.time.LocalDate; + +/** + *

+ * Contains data for the anonymizer record associated with an IP address. + *

+ *

+ * This record is returned by the GeoIP2 Precision Insights web service. + *

+ * + * @param confidence A score ranging from 1 to 99 that is our percent confidence that the + * network is currently part of an actively used VPN service. This is only + * available from the GeoIP2 Precision Insights web service. + * @param isAnonymous Whether the IP address belongs to any sort of anonymous network. + * @param isAnonymousVpn Whether the IP address is registered to an anonymous VPN provider. If a + * VPN provider does not register subnets under names associated with them, + * we will likely only flag their IP ranges using isHostingProvider. + * @param isHostingProvider Whether the IP address belongs to a hosting or VPN provider (see + * description of isAnonymousVpn). + * @param isPublicProxy Whether the IP address belongs to a public proxy. + * @param isResidentialProxy Whether the IP address is on a suspected anonymizing network and + * belongs to a residential ISP. + * @param isTorExitNode Whether the IP address is a Tor exit node. + * @param networkLastSeen The last day that the network was sighted in our analysis of anonymized + * networks. This is only available from the GeoIP2 Precision Insights web + * service. + * @param providerName The name of the VPN provider (e.g., NordVPN, SurfShark, etc.) associated + * with the network. This is only available from the GeoIP2 Precision Insights + * web service. + */ +public record Anonymizer( + @JsonProperty("confidence") + Integer confidence, + + @JsonProperty("is_anonymous") + boolean isAnonymous, + + @JsonProperty("is_anonymous_vpn") + boolean isAnonymousVpn, + + @JsonProperty("is_hosting_provider") + boolean isHostingProvider, + + @JsonProperty("is_public_proxy") + boolean isPublicProxy, + + @JsonProperty("is_residential_proxy") + boolean isResidentialProxy, + + @JsonProperty("is_tor_exit_node") + boolean isTorExitNode, + + @JsonProperty("network_last_seen") + LocalDate networkLastSeen, + + @JsonProperty("provider_name") + String providerName +) implements JsonSerializable { + + /** + * Constructs an {@code Anonymizer} record with {@code null} values for all the nullable + * fields and {@code false} for all boolean fields. + */ + public Anonymizer() { + this(null, false, false, false, false, false, false, null, null); + } +} diff --git a/src/main/java/com/maxmind/geoip2/record/Traits.java b/src/main/java/com/maxmind/geoip2/record/Traits.java index 2540b9d0..040895b8 100644 --- a/src/main/java/com/maxmind/geoip2/record/Traits.java +++ b/src/main/java/com/maxmind/geoip2/record/Traits.java @@ -37,14 +37,33 @@ * address for the system the code is running on. If the system is behind a * NAT, this may differ from the IP address locally assigned to it. * @param isAnonymous This is true if the IP address belongs to any sort of anonymous network. + * This field is deprecated. Please use the anonymizer object from the + * Insights response. * @param isAnonymousVpn This is true if the IP address belongs to an anonymous VPN system. + * This field is deprecated. Please use the anonymizer object from the + * Insights response. * @param isAnycast This is true if the IP address is an anycast address. * @param isHostingProvider This is true if the IP address belongs to a hosting provider. + * This field is deprecated. Please use the anonymizer object from the + * Insights response. * @param isLegitimateProxy This is true if the IP address belongs to a legitimate proxy. * @param isPublicProxy This is true if the IP address belongs to a public proxy. + * This field is deprecated. Please use the anonymizer object from the + * Insights response. * @param isResidentialProxy This is true if the IP address is on a suspected anonymizing network * and belongs to a residential ISP. + * This field is deprecated. Please use the anonymizer object from the + * Insights response. * @param isTorExitNode This is true if the IP address is a Tor exit node. + * This field is deprecated. Please use the anonymizer object from the + * Insights response. + * @param ipRiskSnapshot The risk associated with the IP address. The value ranges from 0.01 to + * 99, with a higher score indicating a higher risk. The IP risk score + * provided in GeoIP products and services is more static than the IP risk + * score provided in minFraud and is not responsive to traffic on your + * network. If you need realtime IP risk scoring based on behavioral signals + * on your own network, please use minFraud. This is only available from the + * Insights web service. * @param isp The name of the ISP associated with the IP address. This is only available from * the City Plus and Insights web services and the Enterprise database. * @param mobileCountryCode The @@ -94,10 +113,12 @@ public record Traits( @MaxMindDbIpAddress InetAddress ipAddress, + @Deprecated(since = "5.0.0", forRemoval = true) @JsonProperty("is_anonymous") @MaxMindDbParameter(name = "is_anonymous", useDefault = true) boolean isAnonymous, + @Deprecated(since = "5.0.0", forRemoval = true) @JsonProperty("is_anonymous_vpn") @MaxMindDbParameter(name = "is_anonymous_vpn", useDefault = true) boolean isAnonymousVpn, @@ -106,6 +127,7 @@ public record Traits( @MaxMindDbParameter(name = "is_anycast", useDefault = true) boolean isAnycast, + @Deprecated(since = "5.0.0", forRemoval = true) @JsonProperty("is_hosting_provider") @MaxMindDbParameter(name = "is_hosting_provider", useDefault = true) boolean isHostingProvider, @@ -114,18 +136,25 @@ public record Traits( @MaxMindDbParameter(name = "is_legitimate_proxy", useDefault = true) boolean isLegitimateProxy, + @Deprecated(since = "5.0.0", forRemoval = true) @JsonProperty("is_public_proxy") @MaxMindDbParameter(name = "is_public_proxy", useDefault = true) boolean isPublicProxy, + @Deprecated(since = "5.0.0", forRemoval = true) @JsonProperty("is_residential_proxy") @MaxMindDbParameter(name = "is_residential_proxy", useDefault = true) boolean isResidentialProxy, + @Deprecated(since = "5.0.0", forRemoval = true) @JsonProperty("is_tor_exit_node") @MaxMindDbParameter(name = "is_tor_exit_node", useDefault = true) boolean isTorExitNode, + @JsonProperty("ip_risk_snapshot") + @MaxMindDbParameter(name = "ip_risk_snapshot") + Double ipRiskSnapshot, + @JsonProperty("isp") @MaxMindDbParameter(name = "isp") String isp, @@ -168,7 +197,7 @@ public Traits() { this(null, null, (ConnectionType) null, null, null, false, false, false, false, false, false, false, false, null, - null, null, null, null, null, null, null); + null, null, null, null, null, null, null, null); } /** diff --git a/src/test/java/com/maxmind/geoip2/model/InsightsResponseTest.java b/src/test/java/com/maxmind/geoip2/model/InsightsResponseTest.java index ae76aed2..38905e77 100644 --- a/src/test/java/com/maxmind/geoip2/model/InsightsResponseTest.java +++ b/src/test/java/com/maxmind/geoip2/model/InsightsResponseTest.java @@ -15,6 +15,7 @@ import com.maxmind.geoip2.WebServiceClient; import com.maxmind.geoip2.exception.GeoIp2Exception; import com.maxmind.geoip2.model.ConnectionTypeResponse.ConnectionType; +import com.maxmind.geoip2.record.Anonymizer; import com.maxmind.geoip2.record.Location; import com.maxmind.geoip2.record.MaxMind; import com.maxmind.geoip2.record.Postal; @@ -23,6 +24,7 @@ import java.io.IOException; import java.net.InetAddress; import java.net.URISyntaxException; +import java.time.LocalDate; import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -168,6 +170,42 @@ public void testTraits() { traits.userCount(), "traits.userCount() does not return 2" ); + assertEquals( + Double.valueOf(0.01), + traits.ipRiskSnapshot(), + "traits.ipRiskSnapshot() does not return 0.01" + ); + } + + @Test + public void testAnonymizer() { + Anonymizer anonymizer = this.insights.anonymizer(); + + assertNotNull(anonymizer, "insights.anonymizer() returns null"); + assertEquals( + Integer.valueOf(99), + anonymizer.confidence(), + "anonymizer.confidence() does not return 99" + ); + assertTrue(anonymizer.isAnonymous(), "anonymizer.isAnonymous() returns true"); + assertTrue(anonymizer.isAnonymousVpn(), "anonymizer.isAnonymousVpn() returns true"); + assertTrue(anonymizer.isHostingProvider(), "anonymizer.isHostingProvider() returns true"); + assertTrue(anonymizer.isPublicProxy(), "anonymizer.isPublicProxy() returns true"); + assertTrue( + anonymizer.isResidentialProxy(), + "anonymizer.isResidentialProxy() returns true" + ); + assertTrue(anonymizer.isTorExitNode(), "anonymizer.isTorExitNode() returns true"); + assertEquals( + LocalDate.parse("2024-12-31"), + anonymizer.networkLastSeen(), + "anonymizer.networkLastSeen() does not return 2024-12-31" + ); + assertEquals( + "NordVPN", + anonymizer.providerName(), + "anonymizer.providerName() does not return NordVPN" + ); } @Test diff --git a/src/test/java/com/maxmind/geoip2/model/JsonTest.java b/src/test/java/com/maxmind/geoip2/model/JsonTest.java index 73aab8da..97e69a8b 100644 --- a/src/test/java/com/maxmind/geoip2/model/JsonTest.java +++ b/src/test/java/com/maxmind/geoip2/model/JsonTest.java @@ -5,6 +5,7 @@ import com.fasterxml.jackson.databind.InjectableValues; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.json.JsonMapper; import com.fasterxml.jackson.jr.ob.JSON; import java.io.IOException; @@ -46,10 +47,22 @@ public void testInsightsSerialization() throws IOException { .put("network", "1.2.3.0/24") .put("organization", "Blorg") .put("user_type", "college") + .put("ip_risk_snapshot", 0.01) // This is here just to simplify the testing. We expect the // difference .put("is_legitimate_proxy", false) .end() + .startObjectField("anonymizer") + .put("confidence", 99) + .put("is_anonymous", true) + .put("is_anonymous_vpn", true) + .put("is_hosting_provider", true) + .put("is_public_proxy", true) + .put("is_residential_proxy", true) + .put("is_tor_exit_node", true) + .put("network_last_seen", "2024-12-31") + .put("provider_name", "NordVPN") + .end() .startObjectField("country") .startObjectField("names") .put("en", "United States of America") @@ -334,6 +347,8 @@ public void testIspSerialization() throws Exception { throws IOException { JsonMapper mapper = JsonMapper.builder() .disable(MapperFeature.CAN_OVERRIDE_ACCESS_MODIFIERS) + .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .addModule(new com.fasterxml.jackson.datatype.jsr310.JavaTimeModule()) .addModule(new com.maxmind.geoip2.InetAddressModule()) .build(); InjectableValues inject = new InjectableValues.Std().addValue( diff --git a/src/test/resources/test-data/insights0.json b/src/test/resources/test-data/insights0.json index 9cbe65ed..45118585 100644 --- a/src/test/resources/test-data/insights0.json +++ b/src/test/resources/test-data/insights0.json @@ -82,6 +82,18 @@ "organization": "Blorg", "static_ip_score": 1.3, "user_count": 2, - "user_type": "college" + "user_type": "college", + "ip_risk_snapshot": 0.01 + }, + "anonymizer": { + "confidence": 99, + "is_anonymous": true, + "is_anonymous_vpn": true, + "is_hosting_provider": true, + "is_public_proxy": true, + "is_residential_proxy": true, + "is_tor_exit_node": true, + "network_last_seen": "2024-12-31", + "provider_name": "NordVPN" } } \ No newline at end of file diff --git a/src/test/resources/test-data/insights1.json b/src/test/resources/test-data/insights1.json index e4b5bf4f..6cc70cba 100644 --- a/src/test/resources/test-data/insights1.json +++ b/src/test/resources/test-data/insights1.json @@ -98,6 +98,18 @@ "organization": "Blorg", "static_ip_score": 1.3, "user_count": 2, - "user_type": "college" + "user_type": "college", + "ip_risk_snapshot": 0.01 + }, + "anonymizer": { + "confidence": 99, + "is_anonymous": true, + "is_anonymous_vpn": true, + "is_hosting_provider": true, + "is_public_proxy": true, + "is_residential_proxy": true, + "is_tor_exit_node": true, + "network_last_seen": "2024-12-31", + "provider_name": "NordVPN" } } \ No newline at end of file