Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -65,12 +65,22 @@ Add the following dependency to your project
<dependency>
<groupId>org.opensky</groupId>
<artifactId>opensky-api</artifactId>
<version>1.3.0</version>
<version>1.4.0</version>
</dependency>
```

### 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());
Expand All @@ -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 {
Expand Down
24 changes: 12 additions & 12 deletions java/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,23 @@

<groupId>org.opensky</groupId>
<artifactId>opensky-api</artifactId>
<version>1.3.0</version>
<version>1.4.0</version>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.7.0</version>
<version>3.13.0</version>
<configuration>
<source>1.7</source>
<target>1.7</target>
<release>17</release>
<source>17</source>
<target>17</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>3.0.1</version>
<version>3.3.1</version>
<executions>
<execution>
<id>attach-sources</id>
Expand All @@ -34,7 +35,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>2.10.4</version>
<version>3.10.1</version>
<executions>
<execution>
<id>attach-javadocs</id>
Expand Down Expand Up @@ -86,19 +87,18 @@
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>3.6.0</version>
<version>4.12.0</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.12.7.1</version>
<version>2.18.2</version>
</dependency>

<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.1</version>
<scope>test</scope>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.11.4</version>
</dependency>

</dependencies>
Expand Down
62 changes: 62 additions & 0 deletions java/src/main/java/org/opensky/api/OpenSkyApi.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@
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;

import java.io.IOException;
import java.io.InputStreamReader;
import java.net.MalformedURLException;
import java.nio.charset.Charset;
import java.time.LocalDateTime;
import java.util.*;

/**
Expand Down Expand Up @@ -55,6 +57,45 @@ 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.
* <p>
* 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.
* <p>
* If the token is expired or unavailable, a new token is fetched from the {@link OpenSkyAuthentication}.
*/
private static class AuthBearerTokenInterceptor implements Interceptor {

private final OpenSkyAuthentication auth;
private String token;
private LocalDateTime expirationTime;

AuthBearerTokenInterceptor(OpenSkyAuthentication auth) {
this.auth = auth;
this.token = "";
this.expirationTime = null;
}

@Override
public Response intercept(@NotNull Chain chain) throws IOException {
LocalDateTime now = LocalDateTime.now();

if (token.isEmpty() || expirationTime == null || now.isAfter(expirationTime)) {
token = auth.accessToken();
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.
*/
Expand All @@ -66,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<>();
Expand All @@ -86,6 +129,25 @@ public OpenSkyApi(String username, String password) {
}
}

public OpenSkyApi(OpenSkyAuthentication auth) {
lastRequestTime = new HashMap<>();
// set up JSON mapper
mapper = new ObjectMapper();
SimpleModule sm = new SimpleModule();
sm.addDeserializer(OpenSkyStates.class, new OpenSkyStatesDeserializer());
mapper.registerModule(sm);

authenticated = auth != null;

if (authenticated) {
okHttpClient = new OkHttpClient.Builder()
.addInterceptor(new AuthBearerTokenInterceptor(auth))
.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
Expand Down
63 changes: 63 additions & 0 deletions java/src/main/java/org/opensky/api/OpenSkyAuthentication.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
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 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.
* 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";


public String accessToken() {
// 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);
return jsonNode.get("access_token").asText();
} else {
throw new RuntimeException("Failed to fetch access token. Response: " + response);
}
} catch (IOException e) {
throw new RuntimeException(e);
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -87,16 +88,19 @@ private Collection<StateVector> 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 {
Expand All @@ -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());
}
}
}
24 changes: 12 additions & 12 deletions java/src/test/java/TestOpenSkyApi.java
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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);
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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);
Expand All @@ -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());
}

Expand All @@ -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");
}
Expand All @@ -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
Expand Down
Loading