Skip to content

Commit 1af3a16

Browse files
committed
Use gson for restclient json handling, add mockito for client tests, add delay to object tree popover, fix missing highlighting of bounding boxes on object tree item hover.
1 parent 22d4cdf commit 1af3a16

19 files changed

+263
-194
lines changed

build.gradle

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,12 @@ dependencies {
5858
// Hamcrest https://mvnrepository.com/artifact/org.hamcrest/hamcrest
5959
testImplementation 'org.hamcrest:hamcrest:2.2'
6060

61+
// Mockito https://mvnrepository.com/artifact/org.mockito/mockito-inline
62+
testImplementation 'org.mockito:mockito-inline:3.5.13'
63+
64+
// Mockito-Junit https://mvnrepository.com/artifact/org.mockito/mockito-junit-jupiter
65+
testImplementation 'org.mockito:mockito-junit-jupiter:3.5.13'
66+
6167
// Commons Collections https://mvnrepository.com/artifact/org.apache.commons/commons-collections4
6268
implementation 'org.apache.commons:commons-collections4:4.4'
6369

@@ -81,22 +87,21 @@ dependencies {
8187
// Jersey REST client https://mvnrepository.com/artifact/org.glassfish.jersey.core/jersey-client
8288
implementation 'org.glassfish.jersey.core:jersey-client:2.32'
8389

84-
// https://mvnrepository.com/artifact/javax.xml.bind/jaxb-api
85-
//implementation 'javax.xml.bind:jaxb-api:2.3.1'
90+
// HK2 InjectionManager https://mvnrepository.com/artifact/org.glassfish.jersey.inject/jersey-hk2
8691
implementation 'org.glassfish.jersey.inject:jersey-hk2:2.32'
87-
// https://mvnrepository.com/artifact/org.glassfish.jersey.media/jersey-media-json-jackson
88-
implementation 'org.glassfish.jersey.media:jersey-media-json-jackson:2.32'
8992

90-
// https://mvnrepository.com/artifact/org.glassfish.jersey.media/jersey-media-multipart
93+
// Jersey Multipart https://mvnrepository.com/artifact/org.glassfish.jersey.media/jersey-media-multipart
9194
implementation 'org.glassfish.jersey.media:jersey-media-multipart:2.32'
9295

93-
// https://mvnrepository.com/artifact/org.jvnet.mimepull/mimepull
96+
// Mimepull https://mvnrepository.com/artifact/org.jvnet.mimepull/mimepull
9497
implementation 'org.jvnet.mimepull:mimepull:1.9.13'
98+
99+
// Xml bind https://mvnrepository.com/artifact/javax.xml.bind/jaxb-api
100+
implementation 'javax.xml.bind:jaxb-api:2.3.1'
95101
}
96102

97103
javafx {
98104
version = '15'
99-
// javafx.swing necessary for screenshot capturing in tests
100105
modules = ['javafx.controls', 'javafx.swing']
101106
}
102107

src/main/java/com/github/mfl28/boundingboxeditor/controller/Controller.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,11 @@
2323
import com.github.mfl28.boundingboxeditor.model.data.ImageMetaData;
2424
import com.github.mfl28.boundingboxeditor.model.data.IoMetaData;
2525
import com.github.mfl28.boundingboxeditor.model.data.ObjectCategory;
26-
import com.github.mfl28.boundingboxeditor.model.io.*;
26+
import com.github.mfl28.boundingboxeditor.model.io.FileChangeWatcher;
27+
import com.github.mfl28.boundingboxeditor.model.io.ImageAnnotationLoadStrategy;
28+
import com.github.mfl28.boundingboxeditor.model.io.ImageAnnotationSaveStrategy;
29+
import com.github.mfl28.boundingboxeditor.model.io.restclients.BoundingBoxPredictorClient;
30+
import com.github.mfl28.boundingboxeditor.model.io.restclients.BoundingBoxPredictorClientConfig;
2731
import com.github.mfl28.boundingboxeditor.model.io.results.*;
2832
import com.github.mfl28.boundingboxeditor.model.io.services.*;
2933
import com.github.mfl28.boundingboxeditor.ui.*;
@@ -310,7 +314,7 @@ public void onRegisterModelNameFetchingAction() {
310314
modelNameFetchService.reset();
311315
final BoundingBoxPredictorClientConfig clientConfig = new BoundingBoxPredictorClientConfig();
312316
view.getSettingsDialog().getInferenceSettings().applyDisplayedSettingsToPredictorClientConfig(clientConfig);
313-
modelNameFetchService.setClientConfig(clientConfig);
317+
modelNameFetchService.setClient(BoundingBoxPredictorClient.create(clientConfig));
314318
modelNameFetchService.restart();
315319
}
316320

src/main/java/com/github/mfl28/boundingboxeditor/model/Model.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@
1919
package com.github.mfl28.boundingboxeditor.model;
2020

2121
import com.github.mfl28.boundingboxeditor.model.data.*;
22-
import com.github.mfl28.boundingboxeditor.model.io.BoundingBoxPredictorClientConfig;
2322
import com.github.mfl28.boundingboxeditor.model.io.BoundingBoxPredictorConfig;
23+
import com.github.mfl28.boundingboxeditor.model.io.restclients.BoundingBoxPredictorClientConfig;
2424
import com.github.mfl28.boundingboxeditor.model.io.results.IOResult;
2525
import javafx.beans.property.BooleanProperty;
2626
import javafx.beans.property.IntegerProperty;

src/main/java/com/github/mfl28/boundingboxeditor/model/io/BoundingBoxPredictor.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package com.github.mfl28.boundingboxeditor.model.io;
22

33
import com.github.mfl28.boundingboxeditor.model.data.*;
4+
import com.github.mfl28.boundingboxeditor.model.io.restclients.BoundingBoxPredictionEntry;
5+
import com.github.mfl28.boundingboxeditor.model.io.restclients.BoundingBoxPredictorClient;
46
import com.github.mfl28.boundingboxeditor.model.io.results.BoundingBoxPredictionResult;
57
import com.github.mfl28.boundingboxeditor.model.io.results.IOErrorInfoEntry;
68
import com.github.mfl28.boundingboxeditor.utils.ColorUtils;

src/main/java/com/github/mfl28/boundingboxeditor/model/io/BoundingBoxPredictionEntry.java renamed to src/main/java/com/github/mfl28/boundingboxeditor/model/io/restclients/BoundingBoxPredictionEntry.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package com.github.mfl28.boundingboxeditor.model.io;
1+
package com.github.mfl28.boundingboxeditor.model.io.restclients;
22

33
import java.util.List;
44
import java.util.Map;

src/main/java/com/github/mfl28/boundingboxeditor/model/io/BoundingBoxPredictorClient.java renamed to src/main/java/com/github/mfl28/boundingboxeditor/model/io/restclients/BoundingBoxPredictorClient.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package com.github.mfl28.boundingboxeditor.model.io;
1+
package com.github.mfl28.boundingboxeditor.model.io.restclients;
22

33
import java.io.InputStream;
44
import java.security.InvalidParameterException;
@@ -15,5 +15,7 @@ static BoundingBoxPredictorClient create(BoundingBoxPredictorClientConfig client
1515

1616
List<BoundingBoxPredictionEntry> predict(InputStream input) throws PredictionClientException;
1717

18+
List<TorchServeRestClient.ModelEntry> models() throws PredictionClientException;
19+
1820
enum ServiceType {TORCH_SERVE}
1921
}

src/main/java/com/github/mfl28/boundingboxeditor/model/io/BoundingBoxPredictorClientConfig.java renamed to src/main/java/com/github/mfl28/boundingboxeditor/model/io/restclients/BoundingBoxPredictorClientConfig.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package com.github.mfl28.boundingboxeditor.model.io;
1+
package com.github.mfl28.boundingboxeditor.model.io.restclients;
22

33
import javafx.beans.property.*;
44

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package com.github.mfl28.boundingboxeditor.model.io.restclients;
2+
3+
import com.google.gson.*;
4+
import com.google.gson.reflect.TypeToken;
5+
6+
import javax.ws.rs.WebApplicationException;
7+
import javax.ws.rs.core.MediaType;
8+
import javax.ws.rs.core.MultivaluedMap;
9+
import javax.ws.rs.ext.MessageBodyReader;
10+
import javax.ws.rs.ext.MessageBodyWriter;
11+
import java.io.*;
12+
import java.lang.annotation.Annotation;
13+
import java.lang.reflect.Type;
14+
import java.nio.charset.StandardCharsets;
15+
import java.util.HashMap;
16+
import java.util.List;
17+
import java.util.Map;
18+
19+
public class GsonMessageBodyHandler implements MessageBodyReader<Object>, MessageBodyWriter<Object> {
20+
private final Gson gson = new GsonBuilder().registerTypeAdapter(BoundingBoxPredictionEntry.class,
21+
new BoundingBoxPredictionEntryDeserializer())
22+
.create();
23+
24+
@Override
25+
public boolean isReadable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
26+
return true;
27+
}
28+
29+
@Override
30+
public Object readFrom(Class<Object> type, Type genericType, Annotation[] annotations, MediaType mediaType,
31+
MultivaluedMap<String, String> httpHeaders, InputStream entityStream)
32+
throws IOException, WebApplicationException {
33+
try(final InputStreamReader inputStreamReader = new InputStreamReader(entityStream, StandardCharsets.UTF_8)) {
34+
return gson.fromJson(inputStreamReader, genericType);
35+
}
36+
}
37+
38+
@Override
39+
public boolean isWriteable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
40+
return true;
41+
}
42+
43+
@Override
44+
public void writeTo(Object o, Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType,
45+
MultivaluedMap<String, Object> httpHeaders, OutputStream entityStream)
46+
throws IOException, WebApplicationException {
47+
try(final OutputStreamWriter outputStreamWriter = new OutputStreamWriter(entityStream,
48+
StandardCharsets.UTF_8)) {
49+
gson.toJson(o, genericType, outputStreamWriter);
50+
}
51+
}
52+
53+
private static class BoundingBoxPredictionEntryDeserializer
54+
implements JsonDeserializer<BoundingBoxPredictionEntry> {
55+
private static final String SCORE_FIELD_NAME = "score";
56+
57+
@Override
58+
public BoundingBoxPredictionEntry deserialize(JsonElement json, Type typeOfT,
59+
JsonDeserializationContext context) throws JsonParseException {
60+
final JsonObject jsonObject = json.getAsJsonObject();
61+
double score = jsonObject.get(SCORE_FIELD_NAME).getAsDouble();
62+
63+
final Map<String, List<Double>> categoryToBoundingBox = new HashMap<>();
64+
65+
for(Map.Entry<String, JsonElement> entry : jsonObject.entrySet()) {
66+
if(!entry.getKey().equals(SCORE_FIELD_NAME) && entry.getValue().isJsonArray()) {
67+
categoryToBoundingBox.put(entry.getKey(),
68+
context.deserialize(entry.getValue(),
69+
new TypeToken<List<Double>>() {}
70+
.getType()));
71+
}
72+
}
73+
74+
return new BoundingBoxPredictionEntry(categoryToBoundingBox, score);
75+
}
76+
}
77+
}

src/main/java/com/github/mfl28/boundingboxeditor/model/io/PredictionClientException.java renamed to src/main/java/com/github/mfl28/boundingboxeditor/model/io/restclients/PredictionClientException.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package com.github.mfl28.boundingboxeditor.model.io;
1+
package com.github.mfl28.boundingboxeditor.model.io.restclients;
22

33
public class PredictionClientException extends Exception {
44
private static final long serialVersionUID = 4076236914663192340L;

src/main/java/com/github/mfl28/boundingboxeditor/model/io/TorchServeRestClient.java renamed to src/main/java/com/github/mfl28/boundingboxeditor/model/io/restclients/TorchServeRestClient.java

Lines changed: 39 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1-
package com.github.mfl28.boundingboxeditor.model.io;
1+
package com.github.mfl28.boundingboxeditor.model.io.restclients;
22

3-
import com.google.gson.*;
4-
import com.google.gson.reflect.TypeToken;
3+
import com.google.gson.JsonSyntaxException;
54
import org.glassfish.jersey.media.multipart.FormDataMultiPart;
65
import org.glassfish.jersey.media.multipart.MultiPart;
76
import org.glassfish.jersey.media.multipart.MultiPartFeature;
@@ -12,13 +11,12 @@
1211
import javax.ws.rs.client.ClientBuilder;
1312
import javax.ws.rs.client.Entity;
1413
import javax.ws.rs.client.WebTarget;
14+
import javax.ws.rs.core.GenericType;
1515
import javax.ws.rs.core.MediaType;
1616
import javax.ws.rs.core.Response;
1717
import java.io.InputStream;
1818
import java.net.ConnectException;
19-
import java.util.HashMap;
2019
import java.util.List;
21-
import java.util.Map;
2220

2321
public class TorchServeRestClient implements BoundingBoxPredictorClient {
2422
private static final String MODELS_RESOURCE_NAME = "models";
@@ -28,142 +26,93 @@ public class TorchServeRestClient implements BoundingBoxPredictorClient {
2826
private static final String PREDICTIONS_RESOURCE_NAME = "predictions";
2927
private static final String SERVER_PREDICTION_POST_ERROR_MESSAGE =
3028
"Could not get prediction from supplied inference server";
31-
private final Client client = ClientBuilder.newBuilder().register(MultiPartFeature.class).build();
29+
private final Client client = ClientBuilder.newBuilder()
30+
.register(MultiPartFeature.class)
31+
.register(GsonMessageBodyHandler.class)
32+
.build();
3233
private final BoundingBoxPredictorClientConfig clientConfig;
3334

3435
public TorchServeRestClient(BoundingBoxPredictorClientConfig clientConfig) {
3536
this.clientConfig = clientConfig;
36-
client.register(MultiPartFeature.class);
3737
}
3838

39-
public String ping() {
40-
return client.target(clientConfig.getInferenceAddress())
41-
.path("ping")
42-
.request(MediaType.APPLICATION_JSON)
43-
.get()
44-
.readEntity(String.class);
45-
}
46-
47-
public List<ModelEntry> models() throws PredictionClientException {
48-
WebTarget managementTarget;
39+
@Override
40+
public List<BoundingBoxPredictionEntry> predict(InputStream input) throws PredictionClientException {
41+
final MultiPart multiPart = new FormDataMultiPart().bodyPart(new StreamDataBodyPart(DATA_BODY_PART_NAME,
42+
input));
43+
WebTarget predictionTarget;
4944

5045
try {
51-
managementTarget = client.target(clientConfig.getManagementAddress());
46+
predictionTarget = client.target(clientConfig.getInferenceAddress());
5247
} catch(IllegalArgumentException | NullPointerException e) {
53-
throw new PredictionClientException("Invalid torch serve management address and/or port.");
48+
throw new PredictionClientException("Invalid torch serve inference address or port.");
5449
}
5550

5651
Response response;
5752

5853
try {
59-
response = managementTarget
60-
.path(MODELS_RESOURCE_NAME)
61-
.request(MediaType.APPLICATION_JSON)
62-
.get();
54+
response = predictionTarget.path(PREDICTIONS_RESOURCE_NAME).path(clientConfig.getInferenceModelName())
55+
.request(MediaType.APPLICATION_JSON)
56+
.post(Entity.entity(multiPart, multiPart.getMediaType()));
6357
} catch(ProcessingException | IllegalArgumentException | IllegalStateException e) {
6458
if(e.getCause() instanceof ConnectException) {
65-
throw new PredictionClientException("Could not connect to supplied management server.");
59+
throw new PredictionClientException("Could not connect to supplied inference server.");
6660
} else {
67-
throw new PredictionClientException(SERVER_MODELS_READ_ERROR_MESSAGE);
61+
throw new PredictionClientException(SERVER_PREDICTION_POST_ERROR_MESSAGE);
6862
}
6963
}
7064

7165
if(!response.getStatusInfo().equals(Response.Status.OK)) {
72-
throw new PredictionClientException(SERVER_MODELS_READ_ERROR_MESSAGE);
66+
throw new PredictionClientException(SERVER_PREDICTION_POST_ERROR_MESSAGE);
7367
}
7468

75-
String modelsJson;
76-
7769
try {
78-
modelsJson = response.readEntity(String.class);
70+
return response.readEntity(new GenericType<>() {});
7971
} catch(ProcessingException | IllegalStateException e) {
80-
throw new PredictionClientException(SERVER_MODELS_READ_ERROR_MESSAGE);
81-
}
82-
83-
List<ModelEntry> models;
84-
85-
try {
86-
models = new Gson().fromJson(modelsJson, ModelsWrapper.class).getModels();
72+
throw new PredictionClientException(SERVER_PREDICTION_POST_ERROR_MESSAGE);
8773
} catch(JsonSyntaxException e) {
88-
throw new PredictionClientException("Invalid torch serve management server response format for \""
89-
+ MODELS_RESOURCE_NAME + "\" resource.");
74+
throw new PredictionClientException("Invalid torch serve inference server response format for \"" +
75+
PREDICTIONS_RESOURCE_NAME + "\" resource.");
9076
}
91-
92-
return models;
9377
}
9478

9579
@Override
96-
public List<BoundingBoxPredictionEntry> predict(InputStream input) throws PredictionClientException {
97-
final MultiPart multiPart = new FormDataMultiPart().bodyPart(new StreamDataBodyPart(DATA_BODY_PART_NAME,
98-
input));
99-
100-
WebTarget predictionTarget;
80+
public List<ModelEntry> models() throws PredictionClientException {
81+
WebTarget managementTarget;
10182

10283
try {
103-
predictionTarget = client.target(clientConfig.getInferenceAddress());
84+
managementTarget = client.target(clientConfig.getManagementAddress());
10485
} catch(IllegalArgumentException | NullPointerException e) {
105-
throw new PredictionClientException("Invalid torch serve inference address and/or port.");
86+
throw new PredictionClientException("Invalid torch serve management address or port.");
10687
}
10788

10889
Response response;
10990

11091
try {
111-
response = predictionTarget.path(PREDICTIONS_RESOURCE_NAME).path(clientConfig.getInferenceModelName())
112-
.request(MediaType.APPLICATION_JSON)
113-
.post(Entity.entity(multiPart, multiPart.getMediaType()));
92+
response = managementTarget
93+
.path(MODELS_RESOURCE_NAME)
94+
.request(MediaType.APPLICATION_JSON)
95+
.get();
11496
} catch(ProcessingException | IllegalArgumentException | IllegalStateException e) {
11597
if(e.getCause() instanceof ConnectException) {
116-
throw new PredictionClientException("Could not connect to supplied inference server.");
98+
throw new PredictionClientException("Could not connect to supplied management server.");
11799
} else {
118-
throw new PredictionClientException(SERVER_PREDICTION_POST_ERROR_MESSAGE);
100+
throw new PredictionClientException(SERVER_MODELS_READ_ERROR_MESSAGE);
119101
}
120102
}
121103

122104
if(!response.getStatusInfo().equals(Response.Status.OK)) {
123-
throw new PredictionClientException(SERVER_PREDICTION_POST_ERROR_MESSAGE);
105+
throw new PredictionClientException(SERVER_MODELS_READ_ERROR_MESSAGE);
124106
}
125107

126-
String predictionJson;
127-
128108
try {
129-
predictionJson = response.readEntity(String.class);
109+
return response.readEntity(ModelsWrapper.class).getModels();
130110
} catch(ProcessingException | IllegalStateException e) {
131-
throw new PredictionClientException(SERVER_PREDICTION_POST_ERROR_MESSAGE);
132-
}
133-
134-
final Gson gson = new GsonBuilder()
135-
.registerTypeAdapter(BoundingBoxPredictionEntry.class,
136-
(JsonDeserializer<BoundingBoxPredictionEntry>) (json, type, context) ->
137-
{
138-
final JsonObject jsonObject = json.getAsJsonObject();
139-
double score = jsonObject.get("score").getAsDouble();
140-
141-
Map<String, List<Double>> categoryToBoundingBox = new HashMap<>();
142-
143-
for(Map.Entry<String, JsonElement> entry : jsonObject.entrySet()) {
144-
if(!entry.getKey().equals("score") && entry.getValue().isJsonArray()) {
145-
categoryToBoundingBox.put(entry.getKey(),
146-
context.deserialize(entry.getValue(),
147-
new TypeToken<List<Double>>() {}
148-
.getType()));
149-
}
150-
}
151-
152-
return new BoundingBoxPredictionEntry(categoryToBoundingBox, score);
153-
154-
}).create();
155-
156-
List<BoundingBoxPredictionEntry> boundingBoxPredictionEntries;
157-
158-
try {
159-
boundingBoxPredictionEntries =
160-
gson.fromJson(predictionJson, new TypeToken<List<BoundingBoxPredictionEntry>>() {}.getType());
111+
throw new PredictionClientException(SERVER_MODELS_READ_ERROR_MESSAGE);
161112
} catch(JsonSyntaxException e) {
162-
throw new PredictionClientException("Invalid torch serve inference server response format for \"" +
163-
PREDICTIONS_RESOURCE_NAME + "\" resource.");
113+
throw new PredictionClientException("Invalid torch serve management server response format for \""
114+
+ MODELS_RESOURCE_NAME + "\" resource.");
164115
}
165-
166-
return boundingBoxPredictionEntries;
167116
}
168117

169118
public static class ModelEntry {

0 commit comments

Comments
 (0)