Skip to content

Commit 110860d

Browse files
Merge pull request #33 from IPGeolocation/update/3.0.0
Update v3.0.0 release contents
2 parents 34ca466 + 98b2248 commit 110860d

File tree

69 files changed

+3948
-1124
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

69 files changed

+3948
-1124
lines changed

CONTRIBUTING.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
This repository contains the public Java SDK package for the IP Geolocation API.
44

55
## Prerequisites
6-
- Java 21+
6+
- Java 8+
77
- Maven 3.8+
88

99
## Setup
@@ -41,6 +41,12 @@ Live tests consume API credits and are disabled by default.
4141
IPGEO_RUN_LIVE_TESTS=true IPGEO_FREE_KEY=... IPGEO_PAID_KEY=... mvn -Dtest=IpGeolocationLiveIntegrationTest test
4242
```
4343

44+
Optional live hardening tests compare raw API JSON to the typed SDK model and consume additional credits.
45+
46+
```bash
47+
IPGEO_RUN_LIVE_HARDENING=true IPGEO_PAID_KEY=... mvn -Dtest=IpGeolocationLiveFieldParityTest test
48+
```
49+
4450
## Coding and PR Notes
4551
- Keep changes focused and include tests for behavior changes.
4652
- Do not commit real API keys, tokens, or secrets.

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2026 IPGeolocation
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 405 additions & 353 deletions
Large diffs are not rendered by default.

pom.xml

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,17 @@
77
<artifactId>ipgeolocation</artifactId>
88
<version>3.0.0</version>
99
<name>IPGeolocation Java SDK</name>
10-
<description>Enterprise Java SDK for the IP Geolocation API.</description>
10+
<description>Java SDK for IP geolocation, company, ASN, VPN and proxy detection, risk signals, timezone, user-agent parsing, and bulk IP intelligence with the IPGeolocation API.</description>
1111
<url>https://ipgeolocation.io/</url>
1212
<organization>
1313
<name>IPGeolocation</name>
1414
<url>https://ipgeolocation.io/</url>
1515
</organization>
1616
<licenses>
1717
<license>
18-
<name>Proprietary</name>
19-
<url>https://ipgeolocation.io/tos.html</url>
18+
<name>MIT License</name>
19+
<url>https://opensource.org/license/mit/</url>
20+
<distribution>repo</distribution>
2021
</license>
2122
</licenses>
2223
<developers>
@@ -35,7 +36,7 @@
3536
</scm>
3637

3738
<properties>
38-
<maven.compiler.release>21</maven.compiler.release>
39+
<maven.compiler.release>8</maven.compiler.release>
3940
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
4041
<jackson.version>2.21.1</jackson.version>
4142
<junit.version>5.11.4</junit.version>
@@ -196,7 +197,7 @@
196197
<version>[3.8.0,)</version>
197198
</requireMavenVersion>
198199
<requireJavaVersion>
199-
<version>[21,)</version>
200+
<version>[1.8,)</version>
200201
</requireJavaVersion>
201202
<banDuplicatePomDependencyVersions/>
202203
<dependencyConvergence/>
@@ -233,6 +234,9 @@
233234
<version>${maven.javadoc.plugin.version}</version>
234235
<configuration>
235236
<source>${maven.compiler.release}</source>
237+
<doclint>all,-missing</doclint>
238+
<excludePackageNames>io.ipgeolocation.sdk.internal</excludePackageNames>
239+
<quiet>true</quiet>
236240
</configuration>
237241
<executions>
238242
<execution>

src/main/java/io/ipgeolocation/sdk/ApiResponse.java

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,46 @@
33
import java.util.Objects;
44

55
/**
6-
* Wraps parsed IP Geolocation API response data with SDK metadata.
6+
* Wraps parsed IPGeolocation API response data with SDK metadata.
77
*
88
* @param <T> parsed response body type
9-
* @param data parsed response payload; may be {@code null} for empty API responses
10-
* @param metadata response metadata extracted from headers and SDK execution metrics
119
*/
12-
public record ApiResponse<T>(T data, ApiResponseMetadata metadata) {
13-
public ApiResponse {
14-
Objects.requireNonNull(metadata, "metadata must not be null");
10+
public final class ApiResponse<T> {
11+
private final T data;
12+
private final ApiResponseMetadata metadata;
13+
14+
public ApiResponse(T data, ApiResponseMetadata metadata) {
15+
this.data = data;
16+
this.metadata = Objects.requireNonNull(metadata, "metadata must not be null");
17+
}
18+
19+
public T data() {
20+
return data;
21+
}
22+
23+
public ApiResponseMetadata metadata() {
24+
return metadata;
25+
}
26+
27+
@Override
28+
public boolean equals(Object other) {
29+
if (this == other) {
30+
return true;
31+
}
32+
if (!(other instanceof ApiResponse)) {
33+
return false;
34+
}
35+
ApiResponse<?> that = (ApiResponse<?>) other;
36+
return Objects.equals(data, that.data) && Objects.equals(metadata, that.metadata);
37+
}
38+
39+
@Override
40+
public int hashCode() {
41+
return Objects.hash(data, metadata);
42+
}
43+
44+
@Override
45+
public String toString() {
46+
return "ApiResponse{data=" + data + ", metadata=" + metadata + '}';
1547
}
1648
}
Lines changed: 93 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,60 @@
11
package io.ipgeolocation.sdk;
22

3+
import io.ipgeolocation.sdk.internal.Compat;
4+
import java.util.ArrayList;
35
import java.util.Collections;
46
import java.util.LinkedHashMap;
57
import java.util.List;
68
import java.util.Map;
9+
import java.util.Objects;
710

811
/**
9-
* Metadata extracted from IP Geolocation API response headers and SDK execution.
10-
*
11-
* @param creditsCharged parsed value of {@code X-Credits-Charged}, or {@code null} when missing
12-
* or non-numeric
13-
* @param successfulRecords parsed value of {@code X-Successful-Record}, or {@code null} when
14-
* missing or non-numeric
15-
* @param statusCode HTTP status code of the final response
16-
* @param durationMs total request duration in milliseconds
17-
* @param rawHeaders immutable response header map with original header names
12+
* Metadata extracted from IPGeolocation API response headers and SDK execution.
1813
*/
19-
public record ApiResponseMetadata(
20-
Integer creditsCharged,
21-
Integer successfulRecords,
22-
int statusCode,
23-
long durationMs,
24-
Map<String, List<String>> rawHeaders) {
14+
public final class ApiResponseMetadata {
15+
private final Integer creditsCharged;
16+
private final Integer successfulRecords;
17+
private final int statusCode;
18+
private final long durationMs;
19+
private final Map<String, List<String>> rawHeaders;
2520

26-
public ApiResponseMetadata {
21+
public ApiResponseMetadata(
22+
Integer creditsCharged,
23+
Integer successfulRecords,
24+
int statusCode,
25+
long durationMs,
26+
Map<String, List<String>> rawHeaders) {
2727
if (statusCode < 100 || statusCode > 599) {
2828
throw new IllegalArgumentException("statusCode must be between 100 and 599");
2929
}
3030
if (durationMs < 0) {
3131
throw new IllegalArgumentException("durationMs must be >= 0");
3232
}
33-
rawHeaders = normalizeHeaders(rawHeaders);
33+
this.creditsCharged = creditsCharged;
34+
this.successfulRecords = successfulRecords;
35+
this.statusCode = statusCode;
36+
this.durationMs = durationMs;
37+
this.rawHeaders = normalizeHeaders(rawHeaders);
38+
}
39+
40+
public Integer creditsCharged() {
41+
return creditsCharged;
42+
}
43+
44+
public Integer successfulRecords() {
45+
return successfulRecords;
46+
}
47+
48+
public int statusCode() {
49+
return statusCode;
50+
}
51+
52+
public long durationMs() {
53+
return durationMs;
54+
}
55+
56+
public Map<String, List<String>> rawHeaders() {
57+
return rawHeaders;
3458
}
3559

3660
/**
@@ -41,7 +65,7 @@ public record ApiResponseMetadata(
4165
* @throws IllegalArgumentException when name is null or blank
4266
*/
4367
public List<String> headerValues(String name) {
44-
if (name == null || name.isBlank()) {
68+
if (Compat.isBlank(name)) {
4569
throw new IllegalArgumentException("header name must not be blank");
4670
}
4771
for (Map.Entry<String, List<String>> entry : rawHeaders.entrySet()) {
@@ -50,7 +74,7 @@ public List<String> headerValues(String name) {
5074
return entry.getValue();
5175
}
5276
}
53-
return List.of();
77+
return Collections.emptyList();
5478
}
5579

5680
/**
@@ -67,19 +91,60 @@ public String firstHeaderValue(String name) {
6791

6892
private static Map<String, List<String>> normalizeHeaders(Map<String, List<String>> headers) {
6993
if (headers == null || headers.isEmpty()) {
70-
return Map.of();
94+
return Collections.emptyMap();
7195
}
7296
Map<String, List<String>> normalized = new LinkedHashMap<>(headers.size());
73-
headers.forEach(
74-
(name, values) -> {
75-
if (name == null) {
76-
return;
77-
}
78-
normalized.put(name, values == null || values.isEmpty() ? List.of() : List.copyOf(values));
79-
});
97+
for (Map.Entry<String, List<String>> entry : headers.entrySet()) {
98+
String name = entry.getKey();
99+
List<String> values = entry.getValue();
100+
if (name == null) {
101+
continue;
102+
}
103+
normalized.put(
104+
name,
105+
values == null || values.isEmpty()
106+
? Collections.<String>emptyList()
107+
: Collections.unmodifiableList(new ArrayList<String>(values)));
108+
}
80109
if (normalized.isEmpty()) {
81-
return Map.of();
110+
return Collections.emptyMap();
82111
}
83112
return Collections.unmodifiableMap(normalized);
84113
}
114+
115+
@Override
116+
public boolean equals(Object other) {
117+
if (this == other) {
118+
return true;
119+
}
120+
if (!(other instanceof ApiResponseMetadata)) {
121+
return false;
122+
}
123+
ApiResponseMetadata that = (ApiResponseMetadata) other;
124+
return statusCode == that.statusCode
125+
&& durationMs == that.durationMs
126+
&& Objects.equals(creditsCharged, that.creditsCharged)
127+
&& Objects.equals(successfulRecords, that.successfulRecords)
128+
&& Objects.equals(rawHeaders, that.rawHeaders);
129+
}
130+
131+
@Override
132+
public int hashCode() {
133+
return Objects.hash(creditsCharged, successfulRecords, statusCode, durationMs, rawHeaders);
134+
}
135+
136+
@Override
137+
public String toString() {
138+
return "ApiResponseMetadata{creditsCharged="
139+
+ creditsCharged
140+
+ ", successfulRecords="
141+
+ successfulRecords
142+
+ ", statusCode="
143+
+ statusCode
144+
+ ", durationMs="
145+
+ durationMs
146+
+ ", rawHeaders="
147+
+ rawHeaders
148+
+ '}';
149+
}
85150
}

src/main/java/io/ipgeolocation/sdk/BulkLookupIpGeolocationRequest.java

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package io.ipgeolocation.sdk;
22

33
import io.ipgeolocation.sdk.exceptions.ValidationException;
4+
import io.ipgeolocation.sdk.internal.Compat;
45
import java.util.ArrayList;
56
import java.util.List;
67
import java.util.Objects;
@@ -9,7 +10,7 @@
910
* Request payload and query parameters for the Unified IPGeolocation API bulk-lookup endpoint
1011
* {@code POST /v3/ipgeo-bulk}.
1112
*
12-
* <p>Use this type with the IP Geolocation API at https://ipgeolocation.io.
13+
* <p>Use this type with the IPGeolocation API at https://ipgeolocation.io.
1314
*
1415
* <p>Use {@link Builder} to construct instances. Query values for {@code include},
1516
* {@code fields}, and {@code excludes} are passed through as provided and validated by the API.
@@ -25,11 +26,11 @@ public final class BulkLookupIpGeolocationRequest {
2526
private final ResponseFormat output;
2627

2728
private BulkLookupIpGeolocationRequest(Builder builder) {
28-
this.ips = List.copyOf(builder.ips);
29+
this.ips = Compat.immutableList(builder.ips);
2930
this.lang = builder.lang;
30-
this.include = List.copyOf(builder.include);
31-
this.fields = List.copyOf(builder.fields);
32-
this.excludes = List.copyOf(builder.excludes);
31+
this.include = Compat.immutableList(builder.include);
32+
this.fields = Compat.immutableList(builder.fields);
33+
this.excludes = Compat.immutableList(builder.excludes);
3334
this.userAgent = builder.userAgent;
3435
this.output = builder.output;
3536
}
@@ -195,7 +196,7 @@ public Builder userAgent(String userAgent) {
195196
this.userAgent = null;
196197
return this;
197198
}
198-
if (userAgent.isBlank()) {
199+
if (Compat.isBlank(userAgent)) {
199200
throw new ValidationException("userAgent must not be blank");
200201
}
201202
this.userAgent = userAgent;
@@ -233,7 +234,7 @@ public BulkLookupIpGeolocationRequest build() {
233234
}
234235

235236
private static String requireToken(String value, String field) {
236-
if (value == null || value.isBlank()) {
237+
if (Compat.isBlank(value)) {
237238
throw new ValidationException(field + " value must not be blank");
238239
}
239240
return value;

0 commit comments

Comments
 (0)