1 | package com.reallifedeveloper.common.infrastructure; | |
2 | ||
3 | import java.time.ZonedDateTime; | |
4 | import java.time.format.DateTimeFormatter; | |
5 | import java.util.Optional; | |
6 | ||
7 | import org.checkerframework.checker.nullness.qual.NonNull; | |
8 | import org.checkerframework.checker.nullness.qual.Nullable; | |
9 | ||
10 | import com.google.gson.JsonElement; | |
11 | import com.google.gson.JsonNull; | |
12 | import com.google.gson.JsonObject; | |
13 | import com.google.gson.JsonParseException; | |
14 | import com.google.gson.JsonParser; | |
15 | ||
16 | import com.reallifedeveloper.common.application.notification.NotificationReader; | |
17 | import com.reallifedeveloper.common.domain.ErrorHandling; | |
18 | ||
19 | /** | |
20 | * An implementation of the {@link NotificationReader} interface that works with JSON as the serialized form, using | |
21 | * <a href="https://code.google.com/p/google-gson/">Gson</a> to parse the JSON string. | |
22 | * | |
23 | * @author RealLifeDeveloper | |
24 | */ | |
25 | @SuppressWarnings("PMD.UnnecessaryCast") // We use casts to @NonNull that PMD considers unnecessary. | |
26 | public final class GsonNotificationReader implements NotificationReader { | |
27 | ||
28 | private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern(GsonObjectSerializer.DATE_TIME_FORMAT); | |
29 | ||
30 | private final JsonObject notification; | |
31 | private final JsonObject event; | |
32 | ||
33 | /** | |
34 | * Creates a new {@code GsonNotificationReader} that parses the given JSON-serialized notification. | |
35 | * | |
36 | * @param jsonNotification the JSON representation of the notification to read | |
37 | * | |
38 | * @throws IllegalArgumentException if {@code jsonNotification} is {@code null} or not a valid JSON object | |
39 | */ | |
40 | public GsonNotificationReader(String jsonNotification) { | |
41 |
1
1. <init> : removed call to com/reallifedeveloper/common/domain/ErrorHandling::checkNull → KILLED |
ErrorHandling.checkNull("jsonNotification must not be null", jsonNotification); |
42 | try { | |
43 | JsonElement element = JsonParser.parseString(jsonNotification); | |
44 |
1
1. <init> : negated conditional → KILLED |
if (!element.isJsonObject()) { |
45 | throw new IllegalArgumentException("Not a JSON object: " + jsonNotification); | |
46 | } | |
47 | this.notification = element.getAsJsonObject(); | |
48 | } catch (JsonParseException e) { | |
49 | throw new IllegalArgumentException("Not legal JSON: " + jsonNotification, e); | |
50 | } | |
51 |
1
1. <init> : negated conditional → KILLED |
if (JsonUtil.isNull(notification.get("event"))) { |
52 | throw new IllegalArgumentException("event not found in JSON string: " + jsonNotification); | |
53 | } | |
54 | this.event = notification.get("event").getAsJsonObject(); | |
55 | } | |
56 | ||
57 | @Override | |
58 | public String eventType() { | |
59 |
1
1. eventType : replaced return value with "" for com/reallifedeveloper/common/infrastructure/GsonNotificationReader::eventType → KILLED |
return (@NonNull String) JsonUtil.stringValue(notification, "eventType", true); |
60 | } | |
61 | ||
62 | @Override | |
63 | public Long storedEventId() { | |
64 |
1
1. storedEventId : replaced Long return value with 0L for com/reallifedeveloper/common/infrastructure/GsonNotificationReader::storedEventId → KILLED |
return (@NonNull Long) JsonUtil.longValue(notification, "storedEventId", true); |
65 | } | |
66 | ||
67 | @Override | |
68 | public ZonedDateTime occurredOn() { | |
69 |
1
1. occurredOn : replaced return value with null for com/reallifedeveloper/common/infrastructure/GsonNotificationReader::occurredOn → KILLED |
return (@NonNull ZonedDateTime) JsonUtil.zonedDateTimeValue(notification, "occurredOn", true); |
70 | } | |
71 | ||
72 | @Override | |
73 | public Integer eventVersion() { | |
74 |
1
1. eventVersion : replaced Integer return value with 0 for com/reallifedeveloper/common/infrastructure/GsonNotificationReader::eventVersion → KILLED |
return (@NonNull Integer) JsonUtil.intValue(event, "eventVersion", true); |
75 | } | |
76 | ||
77 | @Override | |
78 | public Optional<Integer> eventIntValue(String fieldName) { | |
79 |
1
1. eventIntValue : replaced return value with Optional.empty for com/reallifedeveloper/common/infrastructure/GsonNotificationReader::eventIntValue → KILLED |
return Optional.ofNullable(JsonUtil.intValue(event, fieldName, false)); |
80 | } | |
81 | ||
82 | @Override | |
83 | public Optional<Long> eventLongValue(String fieldName) { | |
84 |
1
1. eventLongValue : replaced return value with Optional.empty for com/reallifedeveloper/common/infrastructure/GsonNotificationReader::eventLongValue → KILLED |
return Optional.ofNullable(JsonUtil.longValue(event, fieldName, false)); |
85 | } | |
86 | ||
87 | @Override | |
88 | public Optional<Double> eventDoubleValue(String fieldName) { | |
89 |
1
1. eventDoubleValue : replaced return value with Optional.empty for com/reallifedeveloper/common/infrastructure/GsonNotificationReader::eventDoubleValue → KILLED |
return Optional.ofNullable(JsonUtil.doubleValue(event, fieldName, false)); |
90 | } | |
91 | ||
92 | @Override | |
93 | public Optional<String> eventStringValue(String fieldName) { | |
94 |
1
1. eventStringValue : replaced return value with Optional.empty for com/reallifedeveloper/common/infrastructure/GsonNotificationReader::eventStringValue → KILLED |
return Optional.ofNullable(JsonUtil.stringValue(event, fieldName, false)); |
95 | } | |
96 | ||
97 | @Override | |
98 | public Optional<ZonedDateTime> zonedDateTimeValue(String fieldName) { | |
99 |
1
1. zonedDateTimeValue : replaced return value with Optional.empty for com/reallifedeveloper/common/infrastructure/GsonNotificationReader::zonedDateTimeValue → KILLED |
return Optional.ofNullable(JsonUtil.zonedDateTimeValue(event, fieldName, false)); |
100 | } | |
101 | ||
102 | private static final class JsonUtil { | |
103 | ||
104 | private static @Nullable Integer intValue(JsonObject object, String fieldName, boolean required) { | |
105 | JsonElement jsonElement = fieldValue(object, fieldName, required); | |
106 |
2
1. intValue : replaced Integer return value with 0 for com/reallifedeveloper/common/infrastructure/GsonNotificationReader$JsonUtil::intValue → KILLED 2. intValue : negated conditional → KILLED |
return isNull(jsonElement) ? null : jsonElement.getAsInt(); |
107 | } | |
108 | ||
109 | private static @Nullable Long longValue(JsonObject object, String fieldName, boolean required) { | |
110 | JsonElement jsonElement = fieldValue(object, fieldName, required); | |
111 |
2
1. longValue : negated conditional → KILLED 2. longValue : replaced Long return value with 0L for com/reallifedeveloper/common/infrastructure/GsonNotificationReader$JsonUtil::longValue → KILLED |
return isNull(jsonElement) ? null : jsonElement.getAsLong(); |
112 | } | |
113 | ||
114 | private static @Nullable Double doubleValue(JsonObject object, String fieldName, boolean required) { | |
115 | JsonElement jsonElement = fieldValue(object, fieldName, required); | |
116 |
2
1. doubleValue : negated conditional → KILLED 2. doubleValue : replaced Double return value with 0 for com/reallifedeveloper/common/infrastructure/GsonNotificationReader$JsonUtil::doubleValue → KILLED |
return isNull(jsonElement) ? null : jsonElement.getAsDouble(); |
117 | } | |
118 | ||
119 | private static @Nullable String stringValue(JsonObject object, String fieldName, boolean required) { | |
120 | JsonElement jsonElement = fieldValue(object, fieldName, required); | |
121 |
2
1. stringValue : replaced return value with "" for com/reallifedeveloper/common/infrastructure/GsonNotificationReader$JsonUtil::stringValue → KILLED 2. stringValue : negated conditional → KILLED |
return isNull(jsonElement) ? null : jsonElement.getAsString(); |
122 | } | |
123 | ||
124 | private static @Nullable ZonedDateTime zonedDateTimeValue(JsonObject object, String fieldName, boolean required) { | |
125 | JsonElement jsonElement = fieldValue(object, fieldName, required); | |
126 |
2
1. zonedDateTimeValue : replaced return value with null for com/reallifedeveloper/common/infrastructure/GsonNotificationReader$JsonUtil::zonedDateTimeValue → KILLED 2. zonedDateTimeValue : negated conditional → KILLED |
return isNull(jsonElement) ? null : ZonedDateTime.parse(jsonElement.getAsString(), DATE_TIME_FORMATTER); |
127 | } | |
128 | ||
129 | /** | |
130 | * Gives the value of a, potentially nested, field in a {@code JsonObject}. The field name can be simple, e.g., "foo", or nested, | |
131 | * e.g., "foo.bar". If the name is nested, the sub-components should be the names of nested objects. | |
132 | * <p> | |
133 | * For example, if {@code fieldName} is "foo.bar.baz", the object "foo" is first looked up in {@code object}, then the object "bar" | |
134 | * is looked up in the result, and finally the value of the field "baz" in the resulting object is returned. | |
135 | * <p> | |
136 | * If {@code required} is {@code true}, this method never returns {@code null}. | |
137 | * | |
138 | * @param rootObject the {@code JsonObject} to use to look up the value of the field | |
139 | * @param fieldName the name of the field to look up, potentially nested, e.g., "foo.bar" | |
140 | * @param required if {@code true}, throws an exception if the field does not exist or is {@code null} | |
141 | * | |
142 | * @return the value of the, potentially nested, field as a {@code JsonElement} | |
143 | * | |
144 | * @throws IllegalArgumentException if {@code required} is {@code true} and the field does not exist or is {@code null} | |
145 | */ | |
146 | private static JsonElement fieldValue(JsonObject rootObject, String fieldName, boolean required) { | |
147 | JsonObject object = rootObject; | |
148 | String[] fieldNames = fieldName.split("\\."); | |
149 |
3
1. fieldValue : negated conditional → KILLED 2. fieldValue : Replaced integer subtraction with addition → KILLED 3. fieldValue : changed conditional boundary → KILLED |
for (int i = 0; i < fieldNames.length - 1; i++) { |
150 | JsonElement element = object.get(fieldNames[i]); | |
151 |
1
1. fieldValue : negated conditional → KILLED |
if (element == null) { |
152 | throw new IllegalArgumentException("Field " + fieldName + " not found: object=" + rootObject); | |
153 | } | |
154 |
1
1. fieldValue : negated conditional → KILLED |
if (!element.isJsonObject()) { |
155 | throw new IllegalArgumentException("Field " + fieldName + " not an object: object=" + rootObject); | |
156 | } | |
157 | object = element.getAsJsonObject(); | |
158 | } | |
159 |
1
1. fieldValue : Replaced integer subtraction with addition → KILLED |
JsonElement jsonElement = object.get(fieldNames[fieldNames.length - 1]); |
160 |
2
1. fieldValue : negated conditional → KILLED 2. fieldValue : negated conditional → KILLED |
if (required && isNull(jsonElement)) { |
161 | throw new IllegalArgumentException("Field " + fieldName + " is missing or null: object=" + rootObject); | |
162 | } | |
163 |
1
1. fieldValue : replaced return value with null for com/reallifedeveloper/common/infrastructure/GsonNotificationReader$JsonUtil::fieldValue → KILLED |
return jsonElement; |
164 | } | |
165 | ||
166 | private static boolean isNull(JsonElement jsonElement) { | |
167 |
3
1. isNull : negated conditional → KILLED 2. isNull : negated conditional → KILLED 3. isNull : replaced boolean return with true for com/reallifedeveloper/common/infrastructure/GsonNotificationReader$JsonUtil::isNull → KILLED |
return jsonElement == null || jsonElement == JsonNull.INSTANCE; |
168 | } | |
169 | } | |
170 | } | |
Mutations | ||
41 |
1.1 |
|
44 |
1.1 |
|
51 |
1.1 |
|
59 |
1.1 |
|
64 |
1.1 |
|
69 |
1.1 |
|
74 |
1.1 |
|
79 |
1.1 |
|
84 |
1.1 |
|
89 |
1.1 |
|
94 |
1.1 |
|
99 |
1.1 |
|
106 |
1.1 2.2 |
|
111 |
1.1 2.2 |
|
116 |
1.1 2.2 |
|
121 |
1.1 2.2 |
|
126 |
1.1 2.2 |
|
149 |
1.1 2.2 3.3 |
|
151 |
1.1 |
|
154 |
1.1 |
|
159 |
1.1 |
|
160 |
1.1 2.2 |
|
163 |
1.1 |
|
167 |
1.1 2.2 3.3 |