Skip to content

Commit

Permalink
Bugfix: An illegal reflective access operation has occurred: java.tim…
Browse files Browse the repository at this point in the history
…e.LocalDateTime.date (#31)

- Adds LocalDateTimeAdapter for Gson to stop using reflective access

Resolves #30
{patch}
  • Loading branch information
nagyesta authored Feb 20, 2022
1 parent 1bd8df7 commit 2acdba2
Show file tree
Hide file tree
Showing 16 changed files with 394 additions and 4 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package com.github.nagyesta.abortmission.core.telemetry;

import com.google.gson.TypeAdapter;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;

import java.io.IOException;
import java.time.LocalDateTime;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Optional;
import java.util.TreeMap;

public class LocalDateTimeAdapter extends TypeAdapter<LocalDateTime> {

private static final String DATE = "date";
private static final String YEAR = "year";
private static final String MONTH = "month";
private static final String DAY = "day";
private static final String TIME = "time";
private static final String HOUR = "hour";
private static final String MINUTE = "minute";
private static final String SECOND = "second";
private static final String NANO = "nano";

@Override
public void write(final JsonWriter out, final LocalDateTime value) throws IOException {
out.beginObject()
.name(DATE).beginObject()
.name(YEAR).value(value.getYear())
.name(MONTH).value(value.getMonthValue())
.name(DAY).value(value.getDayOfMonth())
.endObject()
.name(TIME).beginObject()
.name(HOUR).value(value.getHour())
.name(MINUTE).value(value.getMinute())
.name(SECOND).value(value.getSecond())
.name(NANO).value(value.getNano())
.endObject()
.endObject();
}

@Override
public LocalDateTime read(final JsonReader in) throws IOException {
final Map<String, Map<String, Integer>> map = new TreeMap<>();
in.beginObject();
while (in.hasNext()) {
readDateOrTimePart(in, map, in.nextName());
}
in.endObject();
validate(map);
return LocalDateTime.of(
map.get(DATE).get(YEAR),
map.get(DATE).get(MONTH),
map.get(DATE).get(DAY),
map.get(TIME).get(HOUR),
map.get(TIME).get(MINUTE),
map.get(TIME).get(SECOND),
map.get(TIME).get(NANO)
);
}

private void validate(final Map<String, Map<String, Integer>> map) {
final Map<String, Integer> date = assertNotNull(map.get(DATE), "Date component not found.");
assertNotNull(date.get(YEAR), "Year component not found.");
assertNotNull(date.get(MONTH), "Month component not found.");
assertNotNull(date.get(DAY), "Day component not found.");
final Map<String, Integer> time = assertNotNull(map.get(TIME), "Time component not found.");
assertNotNull(time.get(HOUR), "Hour component not found.");
assertNotNull(time.get(MINUTE), "Minute component not found.");
assertNotNull(time.get(SECOND), "Second component not found.");
final Integer value = time.get(NANO);
final String message = "Nano component not found.";
assertNotNull(value, message);
}

private <T> T assertNotNull(final T value, final String message) {
return Optional.ofNullable(value)
.orElseThrow(() -> new IllegalArgumentException(message));
}

private void readDateOrTimePart(final JsonReader in,
final Map<String, Map<String, Integer>> map,
final String name) throws IOException {
final Map<String, Integer> innerMap = map.computeIfAbsent(name, k -> new LinkedHashMap<>());
in.beginObject();
while (in.hasNext()) {
final String field = in.nextName();
innerMap.put(field, in.nextInt());
}
in.endObject();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@
import com.github.nagyesta.abortmission.core.telemetry.stats.DefaultLaunchTelemetryDataSource;
import com.github.nagyesta.abortmission.core.telemetry.stats.LaunchTelemetry;
import com.github.nagyesta.abortmission.core.telemetry.stats.LaunchTelemetryDataSource;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

import java.io.File;
import java.io.FileOutputStream;
import java.io.OutputStreamWriter;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.util.Optional;

import static com.github.nagyesta.abortmission.core.MissionControl.ABORT_MISSION_REPORT_DIRECTORY;
Expand Down Expand Up @@ -75,7 +76,9 @@ protected Optional<String> reportingRoot() {
protected void writeJson(final LaunchTelemetry telemetry, final File json) {
try (FileOutputStream stream = new FileOutputStream(json);
OutputStreamWriter jsonWriter = new OutputStreamWriter(stream, StandardCharsets.UTF_8)) {
final String jsonReport = new Gson().toJson(telemetry);
final String jsonReport = new GsonBuilder()
.registerTypeAdapter(LocalDateTime.class, new LocalDateTimeAdapter())
.create().toJson(telemetry);
jsonWriter.write(jsonReport);
} catch (final Exception e) {
e.printStackTrace();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.github.nagyesta.abortmission.core.telemetry;

import java.time.LocalDateTime;

public final class FromTo {
private LocalDateTime from;
private LocalDateTime to;

public LocalDateTime getFrom() {
return from;
}

public void setFrom(final LocalDateTime from) {
this.from = from;
}

public LocalDateTime getTo() {
return to;
}

public void setTo(final LocalDateTime to) {
this.to = to;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package com.github.nagyesta.abortmission.core.telemetry;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Stream;

class LocalDateTimeAdapterTest {

public static Stream<Arguments> invalidReadProvider() {
return Stream.<Arguments>builder()
.add(Arguments.of("/json/date/local-date-time-invalid-missing-date.json"))
.add(Arguments.of("/json/date/local-date-time-invalid-missing-time.json"))
.add(Arguments.of("/json/date/local-date-time-invalid-missing-year.json"))
.add(Arguments.of("/json/date/local-date-time-invalid-missing-month.json"))
.add(Arguments.of("/json/date/local-date-time-invalid-missing-day.json"))
.add(Arguments.of("/json/date/local-date-time-invalid-missing-hour.json"))
.add(Arguments.of("/json/date/local-date-time-invalid-missing-minute.json"))
.add(Arguments.of("/json/date/local-date-time-invalid-missing-second.json"))
.add(Arguments.of("/json/date/local-date-time-invalid-missing-nano.json"))
.build();
}

@SuppressWarnings("checkstyle:MagicNumber")
public static Stream<Arguments> validWriteSource() {
return Stream.<Arguments>builder()
.add(Arguments.of(null, "null"))
.add(Arguments.of(LocalDateTime.of(2022, 1, 2, 3, 4, 5, 6),
"{\"date\":{\"year\":2022,\"month\":1,\"day\":2},"
+ "\"time\":{\"hour\":3,\"minute\":4,\"second\":5,\"nano\":6}}"))
.add(Arguments.of(LocalDateTime.of(2000, 12, 31, 23, 59, 59, 999000000),
"{\"date\":{\"year\":2000,\"month\":12,\"day\":31},"
+ "\"time\":{\"hour\":23,\"minute\":59,\"second\":59,\"nano\":999000000}}"))
.build();
}

@ParameterizedTest
@MethodSource("validWriteSource")
void testWriteShouldProduceValidOutputWhenCalled(final LocalDateTime input, final String expected) {
//given
final LocalDateTimeAdapter underTest = new LocalDateTimeAdapter();
final Gson gson = new GsonBuilder().registerTypeAdapter(LocalDateTime.class, underTest).create();

//when
final String actual = gson.toJson(input);

//then
Assertions.assertEquals(expected, actual);
}

@Test
void testReadShouldReadSingleObjectWhenValidDataIsRead() {
//given
final String resourceName = "/json/date/local-date-time-valid.json";
final LocalDateTimeAdapter underTest = new LocalDateTimeAdapter();
final Gson gson = new GsonBuilder().registerTypeAdapter(LocalDateTime.class, underTest).create();

try (InputStream stream = getClass().getResourceAsStream(resourceName);
InputStreamReader reader = new InputStreamReader(Objects.requireNonNull(stream))
) {
//when
final LocalDateTime actual = gson.fromJson(reader, LocalDateTime.class);

//then
final LocalDateTime expected = LocalDateTime.of(2022, 2, 20, 13, 49, 10, 862000000);
Assertions.assertEquals(expected, actual);
} catch (final IOException e) {
Assertions.fail(e.getMessage(), e);
}
}

@Test
void testReadShouldReadComplexObjectWhenValidDataIsRead() {
//given
final String resourceName = "/json/date/local-date-time-valid-complex.json";
final LocalDateTimeAdapter underTest = new LocalDateTimeAdapter();
final Gson gson = new GsonBuilder().registerTypeAdapter(LocalDateTime.class, underTest).create();

try (InputStream stream = getClass().getResourceAsStream(resourceName);
InputStreamReader reader = new InputStreamReader(Objects.requireNonNull(stream))
) {
//when
final Map<String, LocalDateTime> map = new HashMap<>();
final FromTo actual = gson.fromJson(reader, FromTo.class);

//then
final LocalDateTime expectedFrom = LocalDateTime.of(2022, 2, 20, 13, 49, 10, 862000000);
final LocalDateTime expectedTo = LocalDateTime.of(2022, 3, 10, 11, 11, 50, 100000000);
Assertions.assertEquals(expectedFrom, actual.getFrom());
Assertions.assertEquals(expectedTo, actual.getTo());
} catch (final IOException e) {
Assertions.fail(e.getMessage(), e);
}
}

@ParameterizedTest
@MethodSource("invalidReadProvider")
void testReadShouldThrowExceptionWhenInvalidDataIsRead(final String resourceName) {
//given
final LocalDateTimeAdapter underTest = new LocalDateTimeAdapter();
final Gson gson = new GsonBuilder().registerTypeAdapter(LocalDateTime.class, underTest).create();

try (InputStream stream = getClass().getResourceAsStream(resourceName);
InputStreamReader reader = new InputStreamReader(Objects.requireNonNull(stream))
) {
//when
Assertions.assertThrows(IllegalArgumentException.class, () -> gson.fromJson(reader, LocalDateTime.class));

//then exception
} catch (final IOException e) {
Assertions.fail(e.getMessage(), e);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@
import com.github.nagyesta.abortmission.core.MissionControl;
import com.github.nagyesta.abortmission.core.telemetry.stats.DefaultLaunchTelemetry;
import com.github.nagyesta.abortmission.core.telemetry.stats.LaunchTelemetryDataSource;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.time.LocalDateTime;
import java.util.Collections;
import java.util.Optional;

Expand Down Expand Up @@ -53,7 +54,10 @@ void testTestExecutionFinishedShouldSaveProvidedData() throws IOException {
verify(underTest).launchTelemetryDataSource();
final File file = fileCaptor.getValue();
Files.readAllLines(file.toPath())
.forEach(line -> Assertions.assertEquals(new Gson().toJson(telemetry), line));
.forEach(line -> Assertions.assertEquals(new GsonBuilder()
.registerTypeAdapter(LocalDateTime.class, new LocalDateTimeAdapter())
.create()
.toJson(telemetry), line));
Assertions.assertTrue(file.delete());
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"time": {
"hour": 13,
"minute": 49,
"second": 10,
"nano": 862000000
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"date": {
"year": 2022,
"month": 2
},
"time": {
"hour": 13,
"minute": 49,
"second": 10,
"nano": 862000000
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"date": {
"year": 2022,
"month": 2,
"day": 20
},
"time": {
"minute": 49,
"second": 10,
"nano": 862000000
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"date": {
"year": 2022,
"month": 2,
"day": 20
},
"time": {
"hour": 13,
"second": 10,
"nano": 862000000
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"date": {
"year": 2022,
"day": 20
},
"time": {
"hour": 13,
"minute": 49,
"second": 10,
"nano": 862000000
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"date": {
"year": 2022,
"month": 2,
"day": 20
},
"time": {
"hour": 13,
"minute": 49,
"second": 10
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"date": {
"year": 2022,
"month": 2,
"day": 20
},
"time": {
"hour": 13,
"minute": 49,
"nano": 862000000
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"date": {
"year": 2022,
"month": 2,
"day": 20
}
}
Loading

0 comments on commit 2acdba2

Please sign in to comment.