From 6b4ade6d4f4f21a7ef6f6ad43d27c29d1d574738 Mon Sep 17 00:00:00 2001 From: dervism Date: Mon, 30 Jun 2025 03:20:35 +0200 Subject: [PATCH 1/4] - Add Authentication class to handle access token retrieval using client credentials. - Enhance `OpenSkyApi` with bearer token-based authentication support and support for clientId and clientSecret. - Update `OpenSkyStatesDeserializer` for improved error handling and replace deprecated method calls. - Migrate tests from JUnit4 to JUnit5 with structural changes and assertions. - Upgrade dependencies (e.g., OkHttp, Jackson, and JUnit Jupiter) and Maven plugin versions. - Bump Java compatibility to version 17. --- java/pom.xml | 24 ++-- .../java/org/opensky/api/Authentication.java | 65 +++++++++++ .../main/java/org/opensky/api/OpenSkyApi.java | 53 +++++++++ .../model/OpenSkyStatesDeserializer.java | 15 ++- java/src/test/java/TestOpenSkyApi.java | 24 ++-- .../java/TestOpenSkyStatesDeserializer.java | 105 +++++++++--------- 6 files changed, 206 insertions(+), 80 deletions(-) create mode 100644 java/src/main/java/org/opensky/api/Authentication.java diff --git a/java/pom.xml b/java/pom.xml index 109265f..abb7c16 100644 --- a/java/pom.xml +++ b/java/pom.xml @@ -6,22 +6,23 @@ org.opensky opensky-api - 1.3.0 + 1.4.0 org.apache.maven.plugins maven-compiler-plugin - 3.7.0 + 3.13.0 - 1.7 - 1.7 + 17 + 17 + 17 org.apache.maven.plugins maven-source-plugin - 3.0.1 + 3.3.1 attach-sources @@ -34,7 +35,7 @@ org.apache.maven.plugins maven-javadoc-plugin - 2.10.4 + 3.10.1 attach-javadocs @@ -86,19 +87,18 @@ com.squareup.okhttp3 okhttp - 3.6.0 + 4.12.0 com.fasterxml.jackson.core jackson-databind - 2.12.7.1 + 2.18.2 - junit - junit - 4.13.1 - test + org.junit.jupiter + junit-jupiter + 5.11.4 diff --git a/java/src/main/java/org/opensky/api/Authentication.java b/java/src/main/java/org/opensky/api/Authentication.java new file mode 100644 index 0000000..8688afe --- /dev/null +++ b/java/src/main/java/org/opensky/api/Authentication.java @@ -0,0 +1,65 @@ +package org.opensky.api; + +import okhttp3.*; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.JsonNode; + +import java.io.IOException; + +/** + * The Authentication class is responsible for handling authentication + * requests to retrieve an access token from the OpenSky Network's authentication API. + */ +public class Authentication { + + /** + * The API endpoint for retrieving an access token from the OpenSky Network's authentication system. + * This URL is used to send authentication requests with client credentials to obtain access tokens. + */ + private static final String TOKEN_API = + "https://auth.opensky-network.org/auth/realms/opensky-network/protocol/openid-connect/token"; + + /** + * Retrieves an access token from the authentication API using the provided client credentials. + * + * @param clientId the client identifier to authenticate with the API + * @param clientSecret the client secret associated with the client identifier + * @return the access token as a string + * @throws RuntimeException if the token retrieval fails or an I/O error occurs + */ + public String accessToken(String clientId, String clientSecret) { + // Create the OkHttpClient instance + OkHttpClient client = new OkHttpClient(); + + // Build the request body with the required parameters + RequestBody requestBody = new FormBody.Builder() + .add("grant_type", "client_credentials") + .add("client_id", clientId) + .add("client_secret", clientSecret) + .build(); + + // Create the POST request + Request request = new Request.Builder() + .url(TOKEN_API) + .post(requestBody) + .addHeader("Content-Type", "application/x-www-form-urlencoded") + .build(); + + // Execute the request + try (Response response = client.newCall(request).execute()) { + if (response.isSuccessful() && response.body() != null) { + String responseBody = response.body().string(); + ObjectMapper mapper = new ObjectMapper(); + JsonNode jsonNode = mapper.readTree(responseBody); + String accessToken = jsonNode.get("access_token").asText(); + //System.out.println("Access Token: " + accessToken); + return accessToken; + } else { + throw new RuntimeException("Failed to fetch access token. Response: " + response); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + + } +} diff --git a/java/src/main/java/org/opensky/api/OpenSkyApi.java b/java/src/main/java/org/opensky/api/OpenSkyApi.java index 27fc909..611837c 100644 --- a/java/src/main/java/org/opensky/api/OpenSkyApi.java +++ b/java/src/main/java/org/opensky/api/OpenSkyApi.java @@ -6,6 +6,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.module.SimpleModule; import okhttp3.*; +import org.jetbrains.annotations.NotNull; import org.opensky.model.OpenSkyStates; import org.opensky.model.OpenSkyStatesDeserializer; @@ -13,6 +14,7 @@ import java.io.InputStreamReader; import java.net.MalformedURLException; import java.nio.charset.Charset; +import java.time.LocalDateTime; import java.util.*; /** @@ -55,6 +57,38 @@ public Response intercept(Chain chain) throws IOException { } } + private static class AuthBearerTokenInterceptor implements Interceptor { + + private String clientId; + private String clientSecret; + private String token; + private LocalDateTime expirationTime; + + AuthBearerTokenInterceptor(String clientId, String clientSecret) { + this.clientId = clientId; + this.clientSecret = clientSecret; + this.token = ""; + this.expirationTime = null; + } + + @Override + public Response intercept(@NotNull Chain chain) throws IOException { + LocalDateTime now = LocalDateTime.now(); + Authentication auth = new Authentication(); + + if (token.isEmpty() || expirationTime == null || now.isAfter(expirationTime)) { + token = auth.accessToken(clientId, clientSecret); + expirationTime = LocalDateTime.now().plusMinutes(30); + } + + Request req = chain.request() + .newBuilder() + .header("Authorization", "Bearer " + token) + .build(); + return chain.proceed(req); + } + } + /** * Create an instance of the API for anonymous access. */ @@ -86,6 +120,25 @@ public OpenSkyApi(String username, String password) { } } + public OpenSkyApi(String clientId, String clientSecret, boolean useBearerToken) { + lastRequestTime = new HashMap<>(); + // set up JSON mapper + mapper = new ObjectMapper(); + SimpleModule sm = new SimpleModule(); + sm.addDeserializer(OpenSkyStates.class, new OpenSkyStatesDeserializer()); + mapper.registerModule(sm); + + authenticated = useBearerToken && clientId != null && clientSecret != null; + + if (authenticated) { + okHttpClient = new OkHttpClient.Builder() + .addInterceptor(new AuthBearerTokenInterceptor(clientId, clientSecret)) + .build(); + } else { + okHttpClient = new OkHttpClient(); + } + } + /** Make the actual HTTP Request and return the parsed response * @param baseUri base uri to request * @param nvps name value pairs to be sent as query parameters diff --git a/java/src/main/java/org/opensky/model/OpenSkyStatesDeserializer.java b/java/src/main/java/org/opensky/model/OpenSkyStatesDeserializer.java index e6145bd..08272f8 100644 --- a/java/src/main/java/org/opensky/model/OpenSkyStatesDeserializer.java +++ b/java/src/main/java/org/opensky/model/OpenSkyStatesDeserializer.java @@ -4,6 +4,7 @@ import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonToken; import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.deser.std.StdDeserializer; import java.io.IOException; @@ -87,16 +88,19 @@ private Collection deserializeStates(JsonParser jp) throws IOExcept @Override public OpenSkyStates deserialize(JsonParser jp, DeserializationContext dc) throws IOException { if (jp.getCurrentToken() != null && jp.getCurrentToken() != JsonToken.START_OBJECT) { - throw dc.mappingException(OpenSkyStates.class); + throw JsonMappingException.from(dc, + "Cannot map " + + OpenSkyStates.class.getSimpleName() + + ".Expected START_OBJECT but got " + jp.getCurrentToken()); } try { OpenSkyStates res = new OpenSkyStates(); for (jp.nextToken(); jp.getCurrentToken() != null && jp.getCurrentToken() != JsonToken.END_OBJECT; jp.nextToken()) { if (jp.getCurrentToken() == JsonToken.FIELD_NAME) { - if ("time".equalsIgnoreCase(jp.getCurrentName())) { + if ("time".equalsIgnoreCase(jp.currentName())) { int t = jp.nextIntValue(0); res.setTime(t); - } else if ("states".equalsIgnoreCase(jp.getCurrentName())) { + } else if ("states".equalsIgnoreCase(jp.currentName())) { jp.nextToken(); res.setStates(deserializeStates(jp)); } else { @@ -107,7 +111,10 @@ public OpenSkyStates deserialize(JsonParser jp, DeserializationContext dc) throw } return res; } catch (JsonParseException jpe) { - throw dc.mappingException(OpenSkyStates.class); + throw JsonMappingException.from(dc, + "Cannot map " + + OpenSkyStates.class.getSimpleName() + + ".Expected START_OBJECT but got " + jp.getCurrentToken()); } } } diff --git a/java/src/test/java/TestOpenSkyApi.java b/java/src/test/java/TestOpenSkyApi.java index ef4ae86..11f561f 100644 --- a/java/src/test/java/TestOpenSkyApi.java +++ b/java/src/test/java/TestOpenSkyApi.java @@ -1,10 +1,10 @@ -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.opensky.api.OpenSkyApi; import org.opensky.model.OpenSkyStates; import org.opensky.model.StateVector; import java.io.IOException; -import static org.junit.Assert.*; +import static org.junit.jupiter.api.Assertions.*; /** * @author Markus Fuchs, fuchs@opensky-network.org @@ -24,12 +24,12 @@ public void testAnonGetStates() throws IOException, InterruptedException { OpenSkyStates os = api.getStates(0, null); long t1 = System.nanoTime(); System.out.println("Request anonStates time = " + ((t1 - t0) / 1000000) + "ms"); - assertTrue("More than 1 state vector", os.getStates().size() > 1); + assertTrue(os.getStates().size() > 1, "More than 1 state vector"); int time = os.getTime(); // more than two requests withing ten seconds os = api.getStates(0, null); - assertNull("No new data", os); + assertNull(os, "No new data"); // wait ten seconds Thread.sleep(10000); @@ -40,7 +40,7 @@ public void testAnonGetStates() throws IOException, InterruptedException { t1 = System.nanoTime(); System.out.println("Request anonStates time = " + ((t1 - t0) / 1000000) + "ms"); assertNotNull(os); - assertTrue("More than 1 state vector for second valid request", os.getStates().size() > 1); + assertTrue(os.getStates().size() > 1, "More than 1 state vector for second valid request"); assertNotEquals(time, os.getTime()); // test bounding box around Switzerland @@ -75,7 +75,7 @@ public void testAnonGetStates() throws IOException, InterruptedException { } OpenSkyStates os2 = api.getStates(0, null, new OpenSkyApi.BoundingBox(45.8389, 47.8229, 5.9962, 10.5226)); - assertTrue("Much less states in Switzerland area than world-wide", os2.getStates().size() < os.getStates().size() - 200); + assertTrue(os2.getStates().size() < os.getStates().size() - 200, "Much less states in Switzerland area than world-wide"); } // can only be tested with a valid account @@ -88,12 +88,12 @@ public void testAuthGetStates() throws IOException, InterruptedException { OpenSkyApi api = new OpenSkyApi(USERNAME, PASSWORD); OpenSkyStates os = api.getStates(0, null); - assertTrue("More than 1 state vector", os.getStates().size() > 1); + assertTrue(os.getStates().size() > 1, "More than 1 state vector"); int time = os.getTime(); // more than two requests withing ten seconds os = api.getStates(0, null); - assertNull("No new data", os); + assertNull(os, "No new data"); // wait five seconds Thread.sleep(5000); @@ -104,7 +104,7 @@ public void testAuthGetStates() throws IOException, InterruptedException { long t1 = System.nanoTime(); System.out.println("Request authStates time = " + ((t1 - t0) / 1000000) + "ms"); assertNotNull(os); - assertTrue("More than 1 state vector for second valid request", os.getStates().size() > 1); + assertTrue(os.getStates().size() > 1, "More than 1 state vector for second valid request"); assertNotEquals(time, os.getTime()); } @@ -116,8 +116,8 @@ public void testAnonGetMyStates() { fail("Anonymous access of 'myStates' expected"); } catch (IllegalAccessError iae) { // like expected - assertTrue("Mismatched exception message", - iae.getMessage().equals("Anonymous access of 'myStates' not allowed")); + assertTrue(iae.getMessage().equals("Anonymous access of 'myStates' not allowed"), + "Mismatched exception message"); } catch (IOException e) { fail("Request should not be submitted"); } @@ -141,7 +141,7 @@ public void testAuthGetMyStates() throws IOException { OpenSkyApi api = new OpenSkyApi(USERNAME, PASSWORD); OpenSkyStates os = api.getMyStates(0, null, SERIALS); - assertTrue("More than 1 state vector", os.getStates().size() > 1); + assertTrue(os.getStates().size() > 1, "More than 1 state vector"); for (StateVector sv : os.getStates()) { // all states contain at least one of the user's sensors diff --git a/java/src/test/java/TestOpenSkyStatesDeserializer.java b/java/src/test/java/TestOpenSkyStatesDeserializer.java index d472190..c4400dc 100644 --- a/java/src/test/java/TestOpenSkyStatesDeserializer.java +++ b/java/src/test/java/TestOpenSkyStatesDeserializer.java @@ -1,7 +1,7 @@ import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.module.SimpleModule; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.opensky.model.OpenSkyStates; import org.opensky.model.OpenSkyStatesDeserializer; import org.opensky.model.StateVector; @@ -9,7 +9,8 @@ import java.io.IOException; import java.util.Iterator; -import static org.junit.Assert.*; +import static java.lang.Double.valueOf; +import static org.junit.jupiter.api.Assertions.*; /** * @author Markus Fuchs, fuchs@opensky-network.org @@ -33,17 +34,17 @@ public class TestOpenSkyStatesDeserializer { "[null,\"ABCDEFG\",\"USA\",1001,1000,1.0,2.0,3.0,false,4.0,5.0,6.0,null]" + "]}"; - @Test(expected = JsonMappingException.class) + @Test() public void testInvalidDeser() throws IOException { ObjectMapper mapper = new ObjectMapper(); SimpleModule sm = new SimpleModule(); sm.addDeserializer(OpenSkyStates.class, new OpenSkyStatesDeserializer()); mapper.registerModule(sm); - mapper.readValue(invalidJson, OpenSkyStates.class); + assertThrows(JsonMappingException.class, () -> mapper.readValue(invalidJson, OpenSkyStates.class)); } - @Test(expected = JsonMappingException.class) + @Test public void testInvalidDeser2() throws IOException { ObjectMapper mapper = new ObjectMapper(); SimpleModule sm = new SimpleModule(); @@ -51,7 +52,7 @@ public void testInvalidDeser2() throws IOException { mapper.registerModule(sm); // ObjectMapper throws Exception here - mapper.readValue("", OpenSkyStates.class); + assertThrows(JsonMappingException.class, () -> mapper.readValue("", OpenSkyStates.class)); } @Test @@ -77,8 +78,8 @@ public void testDeser() throws IOException { mapper.registerModule(sm); OpenSkyStates states = mapper.readValue(validJson, OpenSkyStates.class); - assertEquals("Correct Time", 1002, states.getTime()); - assertEquals("Number states", 6, states.getStates().size()); + assertEquals(1002, states.getTime(), "Correct Time"); + assertEquals(6, states.getStates().size(), "Number states"); // possible cases for state vectors Iterator statesIt = states.getStates().iterator(); @@ -88,17 +89,17 @@ public void testDeser() throws IOException { assertEquals( "cabeef", sv.getIcao24()); assertEquals("ABCDEFG", sv.getCallsign()); assertEquals("USA", sv.getOriginCountry()); - assertEquals(new Double(1001), sv.getLastPositionUpdate()); - assertEquals(new Double(1000), sv.getLastContact()); - assertEquals(new Double(1.0), sv.getLongitude()); - assertEquals(new Double(2.0), sv.getLatitude()); - assertEquals(new Double(3.0), sv.getBaroAltitude()); - assertEquals(new Double(4.0), sv.getVelocity()); - assertEquals(new Double(5.0), sv.getHeading()); - assertEquals(new Double(6.0), sv.getVerticalRate()); + assertEquals(valueOf(1001), sv.getLastPositionUpdate()); + assertEquals(valueOf(1000), sv.getLastContact()); + assertEquals(valueOf(1.0), sv.getLongitude()); + assertEquals(valueOf(2.0), sv.getLatitude()); + assertEquals(valueOf(3.0), sv.getBaroAltitude()); + assertEquals(valueOf(4.0), sv.getVelocity()); + assertEquals(valueOf(5.0), sv.getHeading()); + assertEquals(valueOf(6.0), sv.getVerticalRate()); assertFalse(sv.isOnGround()); assertNull(sv.getSerials()); - assertEquals(new Double(6743.7), sv.getGeoAltitude()); + assertEquals(valueOf(6743.7), sv.getGeoAltitude()); assertEquals("6714", sv.getSquawk()); assertFalse(sv.isSpi()); assertEquals(StateVector.PositionSource.ADS_B, sv.getPositionSource()); @@ -109,13 +110,13 @@ public void testDeser() throws IOException { assertNull(sv.getCallsign()); assertEquals("USA", sv.getOriginCountry()); assertNull(sv.getLastPositionUpdate()); - assertEquals(new Double(1000), sv.getLastContact()); + assertEquals(valueOf(1000), sv.getLastContact()); assertNull(sv.getLongitude()); assertNull(sv.getLatitude()); assertNull(sv.getGeoAltitude()); - assertEquals(new Double(4.0), sv.getVelocity()); - assertEquals(new Double(5.0), sv.getHeading()); - assertEquals(new Double(6.0), sv.getVerticalRate()); + assertEquals(valueOf(4.0), sv.getVelocity()); + assertEquals(valueOf(5.0), sv.getHeading()); + assertEquals(valueOf(6.0), sv.getVerticalRate()); assertFalse(sv.isOnGround()); assertNull(sv.getSerials()); assertNull(sv.getBaroAltitude()); @@ -128,17 +129,17 @@ public void testDeser() throws IOException { assertEquals( "cabeef", sv.getIcao24()); assertNull(sv.getCallsign()); assertEquals("USA", sv.getOriginCountry()); - assertEquals(new Double(1001), sv.getLastPositionUpdate()); + assertEquals(valueOf(1001), sv.getLastPositionUpdate()); assertNull(sv.getLastContact()); - assertEquals(new Double(1.0), sv.getLongitude()); - assertEquals(new Double(2.0), sv.getLatitude()); - assertEquals(new Double(3.0), sv.getBaroAltitude()); + assertEquals(valueOf(1.0), sv.getLongitude()); + assertEquals(valueOf(2.0), sv.getLatitude()); + assertEquals(valueOf(3.0), sv.getBaroAltitude()); assertNull(sv.getVelocity()); assertNull(sv.getHeading()); assertNull(sv.getVerticalRate()); assertFalse(sv.isOnGround()); assertNull(sv.getSerials()); - assertEquals(new Double(6743.7), sv.getGeoAltitude()); + assertEquals(valueOf(6743.7), sv.getGeoAltitude()); assertNull(sv.getSquawk()); assertFalse(sv.isSpi()); assertEquals(StateVector.PositionSource.ADS_B, sv.getPositionSource()); @@ -148,17 +149,17 @@ public void testDeser() throws IOException { assertEquals( "cabeef", sv.getIcao24()); assertEquals("ABCDEFG", sv.getCallsign()); assertEquals("USA", sv.getOriginCountry()); - assertEquals(new Double(1001), sv.getLastPositionUpdate()); - assertEquals(new Double(1000), sv.getLastContact()); - assertEquals(new Double(1.0), sv.getLongitude()); - assertEquals(new Double(2.0), sv.getLatitude()); - assertEquals(new Double(3.0), sv.getBaroAltitude()); - assertEquals(new Double(4.0), sv.getVelocity()); - assertEquals(new Double(5.0), sv.getHeading()); - assertEquals(new Double(6.0), sv.getVerticalRate()); + assertEquals(valueOf(1001), sv.getLastPositionUpdate()); + assertEquals(valueOf(1000), sv.getLastContact()); + assertEquals(valueOf(1.0), sv.getLongitude()); + assertEquals(valueOf(2.0), sv.getLatitude()); + assertEquals(valueOf(3.0), sv.getBaroAltitude()); + assertEquals(valueOf(4.0), sv.getVelocity()); + assertEquals(valueOf(5.0), sv.getHeading()); + assertEquals(valueOf(6.0), sv.getVerticalRate()); assertFalse(sv.isOnGround()); assertArrayEquals(new Integer[] {1234, 6543}, sv.getSerials().toArray(new Integer[sv.getSerials().size()])); - assertEquals(new Double(6743.7), sv.getGeoAltitude()); + assertEquals(valueOf(6743.7), sv.getGeoAltitude()); assertEquals("6714", sv.getSquawk()); assertFalse(sv.isSpi()); assertEquals(StateVector.PositionSource.ASTERIX, sv.getPositionSource()); @@ -168,17 +169,17 @@ public void testDeser() throws IOException { assertEquals( "cabeef", sv.getIcao24()); assertEquals("ABCDEFG", sv.getCallsign()); assertEquals("USA", sv.getOriginCountry()); - assertEquals(new Double(1001), sv.getLastPositionUpdate()); - assertEquals(new Double(1000), sv.getLastContact()); - assertEquals(new Double(1.0), sv.getLongitude()); - assertEquals(new Double(2.0), sv.getLatitude()); - assertEquals(new Double(3.0), sv.getBaroAltitude()); - assertEquals(new Double(4.0), sv.getVelocity()); - assertEquals(new Double(5.0), sv.getHeading()); - assertEquals(new Double(6.0), sv.getVerticalRate()); + assertEquals(valueOf(1001), sv.getLastPositionUpdate()); + assertEquals(valueOf(1000), sv.getLastContact()); + assertEquals(valueOf(1.0), sv.getLongitude()); + assertEquals(valueOf(2.0), sv.getLatitude()); + assertEquals(valueOf(3.0), sv.getBaroAltitude()); + assertEquals(valueOf(4.0), sv.getVelocity()); + assertEquals(valueOf(5.0), sv.getHeading()); + assertEquals(valueOf(6.0), sv.getVerticalRate()); assertFalse(sv.isOnGround()); assertArrayEquals(new Integer[] {1234}, sv.getSerials().toArray(new Integer[sv.getSerials().size()])); - assertEquals(new Double(6743.7), sv.getGeoAltitude()); + assertEquals(valueOf(6743.7), sv.getGeoAltitude()); assertEquals("6714", sv.getSquawk()); assertTrue(sv.isSpi()); assertEquals(StateVector.PositionSource.UNKNOWN, sv.getPositionSource()); @@ -189,14 +190,14 @@ public void testDeser() throws IOException { assertEquals( "cabeef", sv.getIcao24()); assertEquals("ABCDEFG", sv.getCallsign()); assertEquals("USA", sv.getOriginCountry()); - assertEquals(new Double(1001), sv.getLastPositionUpdate()); - assertEquals(new Double(1000), sv.getLastContact()); - assertEquals(new Double(1.0), sv.getLongitude()); - assertEquals(new Double(2.0), sv.getLatitude()); - assertEquals(new Double(3.0), sv.getBaroAltitude()); - assertEquals(new Double(4.0), sv.getVelocity()); - assertEquals(new Double(5.0), sv.getHeading()); - assertEquals(new Double(6.0), sv.getVerticalRate()); + assertEquals(valueOf(1001), sv.getLastPositionUpdate()); + assertEquals(valueOf(1000), sv.getLastContact()); + assertEquals(valueOf(1.0), sv.getLongitude()); + assertEquals(valueOf(2.0), sv.getLatitude()); + assertEquals(valueOf(3.0), sv.getBaroAltitude()); + assertEquals(valueOf(4.0), sv.getVelocity()); + assertEquals(valueOf(5.0), sv.getHeading()); + assertEquals(valueOf(6.0), sv.getVerticalRate()); assertTrue(sv.isOnGround()); assertNull(sv.getSerials()); assertNull(sv.getGeoAltitude()); From d61a2ecfc92d761541febe209367cbbfa75b9ea4 Mon Sep 17 00:00:00 2001 From: dervism Date: Mon, 30 Jun 2025 03:25:06 +0200 Subject: [PATCH 2/4] Update README with Maven installation instructions and version bump to 1.4.0 --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 7e60b0d..c388293 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ will output something like this: ## Java API -* Maven project (not yet in a public repository) +* Maven project (not yet in a public repository). Clone this repo and build locally (see installation instructions below). * Uses [```OkHttp```](https://square.github.io/okhttp/) for HTTP requests ### Installation @@ -65,7 +65,7 @@ Add the following dependency to your project org.opensky opensky-api - 1.3.0 + 1.4.0 ``` From 44e089d89a4a106e419c81899c5233533e57e988 Mon Sep 17 00:00:00 2001 From: dervism Date: Mon, 30 Jun 2025 03:28:25 +0200 Subject: [PATCH 3/4] Update README with OAuth and unauthenticated usage examples; bump version to 1.4.0 --- README.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index c388293..85d6355 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,16 @@ Add the following dependency to your project ### Usage +With OAuth clientId / clientSecret: +``` +OpenSkyApi api = new OpenSkyApi("clientId", "clientSecret", true); + OpenSkyStates os = api.getStates(0, null, + new OpenSkyApi.BoundingBox(45.8389, 47.8229, 5.9962, 10.5226)); + + os.getStates().forEach(System.out::println); +``` + +Unauthenticated: ``` OpenSkyStates states = new OpenSkyApi().getStates(0); System.out.println("Number of states: " + states.getStates().size()); @@ -85,7 +95,7 @@ In build.gradle, add the following lines dependencies { /* do not delete the other entries, just add this one */ - compile 'org.opensky:opensky-api:1.3.0' + compile 'org.opensky:opensky-api:1.4.0' } repositories { From 0198d91bda1c0aebcafb0d8d34cc8f822fe028b2 Mon Sep 17 00:00:00 2001 From: dervism Date: Mon, 30 Jun 2025 21:55:53 +0200 Subject: [PATCH 4/4] Refactor `Authentication` class, rename to `OpenSkyAuthentication`, and centralize clientId/clientSecret handling for improved OAuth token management. Update `OpenSkyApi` to use `OpenSkyAuthentication`. Deprecate old username/password-based constructor. --- .../main/java/org/opensky/api/OpenSkyApi.java | 29 ++++++++++++------- ...cation.java => OpenSkyAuthentication.java} | 24 +++++++-------- 2 files changed, 30 insertions(+), 23 deletions(-) rename java/src/main/java/org/opensky/api/{Authentication.java => OpenSkyAuthentication.java} (73%) diff --git a/java/src/main/java/org/opensky/api/OpenSkyApi.java b/java/src/main/java/org/opensky/api/OpenSkyApi.java index 611837c..54696b5 100644 --- a/java/src/main/java/org/opensky/api/OpenSkyApi.java +++ b/java/src/main/java/org/opensky/api/OpenSkyApi.java @@ -57,16 +57,24 @@ public Response intercept(Chain chain) throws IOException { } } + /** + * This class is an implementation of the {@link Interceptor} interface + * used to manage and include the Authorization Bearer Token in HTTP requests. + *

+ * It intercepts outgoing HTTP requests and adds an "Authorization" header with + * a Bearer token. Tokens are fetched from the provided {@link OpenSkyAuthentication} instance, + * and they are cached with an expiration time for reuse to reduce redundant token requests. + *

+ * If the token is expired or unavailable, a new token is fetched from the {@link OpenSkyAuthentication}. + */ private static class AuthBearerTokenInterceptor implements Interceptor { - private String clientId; - private String clientSecret; + private final OpenSkyAuthentication auth; private String token; private LocalDateTime expirationTime; - AuthBearerTokenInterceptor(String clientId, String clientSecret) { - this.clientId = clientId; - this.clientSecret = clientSecret; + AuthBearerTokenInterceptor(OpenSkyAuthentication auth) { + this.auth = auth; this.token = ""; this.expirationTime = null; } @@ -74,10 +82,9 @@ private static class AuthBearerTokenInterceptor implements Interceptor { @Override public Response intercept(@NotNull Chain chain) throws IOException { LocalDateTime now = LocalDateTime.now(); - Authentication auth = new Authentication(); if (token.isEmpty() || expirationTime == null || now.isAfter(expirationTime)) { - token = auth.accessToken(clientId, clientSecret); + token = auth.accessToken(); expirationTime = LocalDateTime.now().plusMinutes(30); } @@ -100,6 +107,8 @@ public OpenSkyApi() { * Create an instance of the API for authenticated access * @param username an OpenSky username * @param password an OpenSky password for the given username + * @deprecated Use OAuth2 clientId/clientSecret authentication flow + * @see #OpenSkyApi(OpenSkyAuthentication) */ public OpenSkyApi(String username, String password) { lastRequestTime = new HashMap<>(); @@ -120,7 +129,7 @@ public OpenSkyApi(String username, String password) { } } - public OpenSkyApi(String clientId, String clientSecret, boolean useBearerToken) { + public OpenSkyApi(OpenSkyAuthentication auth) { lastRequestTime = new HashMap<>(); // set up JSON mapper mapper = new ObjectMapper(); @@ -128,11 +137,11 @@ public OpenSkyApi(String clientId, String clientSecret, boolean useBearerToken) sm.addDeserializer(OpenSkyStates.class, new OpenSkyStatesDeserializer()); mapper.registerModule(sm); - authenticated = useBearerToken && clientId != null && clientSecret != null; + authenticated = auth != null; if (authenticated) { okHttpClient = new OkHttpClient.Builder() - .addInterceptor(new AuthBearerTokenInterceptor(clientId, clientSecret)) + .addInterceptor(new AuthBearerTokenInterceptor(auth)) .build(); } else { okHttpClient = new OkHttpClient(); diff --git a/java/src/main/java/org/opensky/api/Authentication.java b/java/src/main/java/org/opensky/api/OpenSkyAuthentication.java similarity index 73% rename from java/src/main/java/org/opensky/api/Authentication.java rename to java/src/main/java/org/opensky/api/OpenSkyAuthentication.java index 8688afe..a0b59f4 100644 --- a/java/src/main/java/org/opensky/api/Authentication.java +++ b/java/src/main/java/org/opensky/api/OpenSkyAuthentication.java @@ -10,7 +10,14 @@ * The Authentication class is responsible for handling authentication * requests to retrieve an access token from the OpenSky Network's authentication API. */ -public class Authentication { +public class OpenSkyAuthentication { + private final String clientId; + private final String clientSecret; + + public OpenSkyAuthentication(String clientId, String clientSecret) { + this.clientId = clientId; + this.clientSecret = clientSecret; + } /** * The API endpoint for retrieving an access token from the OpenSky Network's authentication system. @@ -19,15 +26,8 @@ public class Authentication { private static final String TOKEN_API = "https://auth.opensky-network.org/auth/realms/opensky-network/protocol/openid-connect/token"; - /** - * Retrieves an access token from the authentication API using the provided client credentials. - * - * @param clientId the client identifier to authenticate with the API - * @param clientSecret the client secret associated with the client identifier - * @return the access token as a string - * @throws RuntimeException if the token retrieval fails or an I/O error occurs - */ - public String accessToken(String clientId, String clientSecret) { + + public String accessToken() { // Create the OkHttpClient instance OkHttpClient client = new OkHttpClient(); @@ -51,9 +51,7 @@ public String accessToken(String clientId, String clientSecret) { String responseBody = response.body().string(); ObjectMapper mapper = new ObjectMapper(); JsonNode jsonNode = mapper.readTree(responseBody); - String accessToken = jsonNode.get("access_token").asText(); - //System.out.println("Access Token: " + accessToken); - return accessToken; + return jsonNode.get("access_token").asText(); } else { throw new RuntimeException("Failed to fetch access token. Response: " + response); }