Skip to content

Commit eba7a73

Browse files
Flossyclaude
andcommitted
fix: Add null validation to ApplicationDescriptorDeserializer
Fixes #196 Added validation for required JSON fields in ApplicationDescriptorDeserializer to prevent NullPointerException when fields are missing. Changes: - Validate required fields (applicationId, name, version, mainClass) exist before calling .asText() on JsonNode - Validate threadPool nested fields (corePoolSize, maxPoolSize, queueCapacity, keepAliveTimeSeconds) exist before calling .asInt()/.asLong() - Throw IOException with clear error messages indicating which field is missing - Created comprehensive test suite with 13 tests covering all validation cases Tests verify: - Missing required fields throw IOException with clear messages - Missing threadPool nested fields throw IOException - Valid JSON deserializes correctly - Round-trip serialization/deserialization works - Optional fields (classpath, properties, threadPool) handled correctly Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent 6fc0cf1 commit eba7a73

2 files changed

Lines changed: 300 additions & 8 deletions

File tree

jplatform-cluster/src/main/java/org/flossware/jplatform/cluster/ApplicationDescriptorJsonModule.java

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -77,11 +77,29 @@ public ApplicationDescriptor deserialize(JsonParser parser, DeserializationConte
7777
throws IOException {
7878
JsonNode node = parser.getCodec().readTree(parser);
7979

80+
// Validate required fields
81+
JsonNode appIdNode = node.get("applicationId");
82+
if (appIdNode == null) {
83+
throw new IOException("Missing required field: applicationId");
84+
}
85+
JsonNode nameNode = node.get("name");
86+
if (nameNode == null) {
87+
throw new IOException("Missing required field: name");
88+
}
89+
JsonNode versionNode = node.get("version");
90+
if (versionNode == null) {
91+
throw new IOException("Missing required field: version");
92+
}
93+
JsonNode mainClassNode = node.get("mainClass");
94+
if (mainClassNode == null) {
95+
throw new IOException("Missing required field: mainClass");
96+
}
97+
8098
ApplicationDescriptor.Builder builder = ApplicationDescriptor.builder()
81-
.applicationId(node.get("applicationId").asText())
82-
.name(node.get("name").asText())
83-
.version(node.get("version").asText())
84-
.mainClass(node.get("mainClass").asText());
99+
.applicationId(appIdNode.asText())
100+
.name(nameNode.asText())
101+
.version(versionNode.asText())
102+
.mainClass(mainClassNode.asText());
85103

86104
// Add classpath entries
87105
JsonNode classpath = node.get("classpath");
@@ -101,11 +119,22 @@ public ApplicationDescriptor deserialize(JsonParser parser, DeserializationConte
101119
// Add thread pool config if present
102120
JsonNode threadPool = node.get("threadPool");
103121
if (threadPool != null) {
122+
// Validate threadPool required fields
123+
JsonNode coreSizeNode = threadPool.get("corePoolSize");
124+
JsonNode maxSizeNode = threadPool.get("maxPoolSize");
125+
JsonNode queueCapNode = threadPool.get("queueCapacity");
126+
JsonNode keepAliveNode = threadPool.get("keepAliveTimeSeconds");
127+
128+
if (coreSizeNode == null || maxSizeNode == null ||
129+
queueCapNode == null || keepAliveNode == null) {
130+
throw new IOException("threadPool object is incomplete - missing required fields");
131+
}
132+
104133
ThreadPoolConfig config = ThreadPoolConfig.builder()
105-
.corePoolSize(threadPool.get("corePoolSize").asInt())
106-
.maxPoolSize(threadPool.get("maxPoolSize").asInt())
107-
.queueCapacity(threadPool.get("queueCapacity").asInt())
108-
.keepAliveTimeSeconds(threadPool.get("keepAliveTimeSeconds").asLong())
134+
.corePoolSize(coreSizeNode.asInt())
135+
.maxPoolSize(maxSizeNode.asInt())
136+
.queueCapacity(queueCapNode.asInt())
137+
.keepAliveTimeSeconds(keepAliveNode.asLong())
109138
.build();
110139
builder.threadPoolConfig(config);
111140
}
Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
package org.flossware.jplatform.cluster;
2+
3+
import com.fasterxml.jackson.databind.ObjectMapper;
4+
import org.flossware.jplatform.api.ApplicationDescriptor;
5+
import org.flossware.jplatform.api.ThreadPoolConfig;
6+
import org.junit.jupiter.api.BeforeEach;
7+
import org.junit.jupiter.api.Test;
8+
9+
import java.io.IOException;
10+
import java.net.URI;
11+
12+
import static org.junit.jupiter.api.Assertions.*;
13+
14+
/**
15+
* Unit tests for ApplicationDescriptorJsonModule serialization/deserialization.
16+
*/
17+
class ApplicationDescriptorJsonModuleTest {
18+
19+
private ObjectMapper mapper;
20+
21+
@BeforeEach
22+
void setUp() {
23+
mapper = new ObjectMapper();
24+
mapper.registerModule(new ApplicationDescriptorJsonModule());
25+
}
26+
27+
@Test
28+
void testSerializeAndDeserialize_validDescriptor() throws IOException {
29+
ApplicationDescriptor original = ApplicationDescriptor.builder()
30+
.applicationId("app1")
31+
.name("Test App")
32+
.version("1.0")
33+
.mainClass("com.test.Main")
34+
.addClasspathEntry(URI.create("file:///lib/test.jar"))
35+
.property("key1", "value1")
36+
.threadPoolConfig(ThreadPoolConfig.builder()
37+
.corePoolSize(5)
38+
.maxPoolSize(10)
39+
.queueCapacity(100)
40+
.keepAliveTimeSeconds(60)
41+
.build())
42+
.enableMessaging(true)
43+
.build();
44+
45+
String json = mapper.writeValueAsString(original);
46+
ApplicationDescriptor deserialized = mapper.readValue(json, ApplicationDescriptor.class);
47+
48+
assertEquals(original.getApplicationId(), deserialized.getApplicationId());
49+
assertEquals(original.getName(), deserialized.getName());
50+
assertEquals(original.getVersion(), deserialized.getVersion());
51+
assertEquals(original.getMainClass(), deserialized.getMainClass());
52+
assertEquals(original.getClasspathEntries(), deserialized.getClasspathEntries());
53+
assertEquals(original.getProperties(), deserialized.getProperties());
54+
assertEquals(original.isEnableMessaging(), deserialized.isEnableMessaging());
55+
56+
ThreadPoolConfig originalPool = original.getThreadPoolConfig();
57+
ThreadPoolConfig deserializedPool = deserialized.getThreadPoolConfig();
58+
assertNotNull(deserializedPool);
59+
assertEquals(originalPool.getCorePoolSize(), deserializedPool.getCorePoolSize());
60+
assertEquals(originalPool.getMaxPoolSize(), deserializedPool.getMaxPoolSize());
61+
assertEquals(originalPool.getQueueCapacity(), deserializedPool.getQueueCapacity());
62+
assertEquals(originalPool.getKeepAliveTimeSeconds(), deserializedPool.getKeepAliveTimeSeconds());
63+
}
64+
65+
@Test
66+
void testDeserialize_missingApplicationId() {
67+
String json = "{\"name\":\"App\",\"version\":\"1.0\",\"mainClass\":\"com.App\"}";
68+
69+
IOException exception = assertThrows(IOException.class,
70+
() -> mapper.readValue(json, ApplicationDescriptor.class));
71+
72+
assertTrue(exception.getMessage().contains("Missing required field: applicationId"));
73+
}
74+
75+
@Test
76+
void testDeserialize_missingName() {
77+
String json = "{\"applicationId\":\"app1\",\"version\":\"1.0\",\"mainClass\":\"com.App\"}";
78+
79+
IOException exception = assertThrows(IOException.class,
80+
() -> mapper.readValue(json, ApplicationDescriptor.class));
81+
82+
assertTrue(exception.getMessage().contains("Missing required field: name"));
83+
}
84+
85+
@Test
86+
void testDeserialize_missingVersion() {
87+
String json = "{\"applicationId\":\"app1\",\"name\":\"App\",\"mainClass\":\"com.App\"}";
88+
89+
IOException exception = assertThrows(IOException.class,
90+
() -> mapper.readValue(json, ApplicationDescriptor.class));
91+
92+
assertTrue(exception.getMessage().contains("Missing required field: version"));
93+
}
94+
95+
@Test
96+
void testDeserialize_missingMainClass() {
97+
String json = "{\"applicationId\":\"app1\",\"name\":\"App\",\"version\":\"1.0\"}";
98+
99+
IOException exception = assertThrows(IOException.class,
100+
() -> mapper.readValue(json, ApplicationDescriptor.class));
101+
102+
assertTrue(exception.getMessage().contains("Missing required field: mainClass"));
103+
}
104+
105+
@Test
106+
void testDeserialize_threadPoolMissingCorePoolSize() {
107+
String json = "{" +
108+
"\"applicationId\":\"app1\"," +
109+
"\"name\":\"App\"," +
110+
"\"version\":\"1.0\"," +
111+
"\"mainClass\":\"com.App\"," +
112+
"\"threadPool\":{" +
113+
"\"maxPoolSize\":10," +
114+
"\"queueCapacity\":100," +
115+
"\"keepAliveTimeSeconds\":60" +
116+
"}" +
117+
"}";
118+
119+
IOException exception = assertThrows(IOException.class,
120+
() -> mapper.readValue(json, ApplicationDescriptor.class));
121+
122+
assertTrue(exception.getMessage().contains("threadPool object is incomplete"));
123+
}
124+
125+
@Test
126+
void testDeserialize_threadPoolMissingMaxPoolSize() {
127+
String json = "{" +
128+
"\"applicationId\":\"app1\"," +
129+
"\"name\":\"App\"," +
130+
"\"version\":\"1.0\"," +
131+
"\"mainClass\":\"com.App\"," +
132+
"\"threadPool\":{" +
133+
"\"corePoolSize\":5," +
134+
"\"queueCapacity\":100," +
135+
"\"keepAliveTimeSeconds\":60" +
136+
"}" +
137+
"}";
138+
139+
IOException exception = assertThrows(IOException.class,
140+
() -> mapper.readValue(json, ApplicationDescriptor.class));
141+
142+
assertTrue(exception.getMessage().contains("threadPool object is incomplete"));
143+
}
144+
145+
@Test
146+
void testDeserialize_threadPoolMissingQueueCapacity() {
147+
String json = "{" +
148+
"\"applicationId\":\"app1\"," +
149+
"\"name\":\"App\"," +
150+
"\"version\":\"1.0\"," +
151+
"\"mainClass\":\"com.App\"," +
152+
"\"threadPool\":{" +
153+
"\"corePoolSize\":5," +
154+
"\"maxPoolSize\":10," +
155+
"\"keepAliveTimeSeconds\":60" +
156+
"}" +
157+
"}";
158+
159+
IOException exception = assertThrows(IOException.class,
160+
() -> mapper.readValue(json, ApplicationDescriptor.class));
161+
162+
assertTrue(exception.getMessage().contains("threadPool object is incomplete"));
163+
}
164+
165+
@Test
166+
void testDeserialize_threadPoolMissingKeepAliveTimeSeconds() {
167+
String json = "{" +
168+
"\"applicationId\":\"app1\"," +
169+
"\"name\":\"App\"," +
170+
"\"version\":\"1.0\"," +
171+
"\"mainClass\":\"com.App\"," +
172+
"\"threadPool\":{" +
173+
"\"corePoolSize\":5," +
174+
"\"maxPoolSize\":10," +
175+
"\"queueCapacity\":100" +
176+
"}" +
177+
"}";
178+
179+
IOException exception = assertThrows(IOException.class,
180+
() -> mapper.readValue(json, ApplicationDescriptor.class));
181+
182+
assertTrue(exception.getMessage().contains("threadPool object is incomplete"));
183+
}
184+
185+
@Test
186+
void testDeserialize_noThreadPool() throws IOException {
187+
String json = "{" +
188+
"\"applicationId\":\"app1\"," +
189+
"\"name\":\"App\"," +
190+
"\"version\":\"1.0\"," +
191+
"\"mainClass\":\"com.App\"" +
192+
"}";
193+
194+
ApplicationDescriptor descriptor = mapper.readValue(json, ApplicationDescriptor.class);
195+
196+
assertEquals("app1", descriptor.getApplicationId());
197+
assertEquals("App", descriptor.getName());
198+
assertEquals("1.0", descriptor.getVersion());
199+
assertEquals("com.App", descriptor.getMainClass());
200+
// When no threadPool is provided, ApplicationDescriptor uses default config
201+
assertNotNull(descriptor.getThreadPoolConfig());
202+
assertEquals(2, descriptor.getThreadPoolConfig().getCorePoolSize());
203+
assertEquals(10, descriptor.getThreadPoolConfig().getMaxPoolSize());
204+
assertEquals(100, descriptor.getThreadPoolConfig().getQueueCapacity());
205+
assertEquals(60, descriptor.getThreadPoolConfig().getKeepAliveTimeSeconds());
206+
}
207+
208+
@Test
209+
void testDeserialize_emptyClasspath() throws IOException {
210+
String json = "{" +
211+
"\"applicationId\":\"app1\"," +
212+
"\"name\":\"App\"," +
213+
"\"version\":\"1.0\"," +
214+
"\"mainClass\":\"com.App\"," +
215+
"\"classpath\":[]" +
216+
"}";
217+
218+
ApplicationDescriptor descriptor = mapper.readValue(json, ApplicationDescriptor.class);
219+
220+
assertEquals("app1", descriptor.getApplicationId());
221+
assertTrue(descriptor.getClasspathEntries().isEmpty());
222+
}
223+
224+
@Test
225+
void testDeserialize_emptyProperties() throws IOException {
226+
String json = "{" +
227+
"\"applicationId\":\"app1\"," +
228+
"\"name\":\"App\"," +
229+
"\"version\":\"1.0\"," +
230+
"\"mainClass\":\"com.App\"," +
231+
"\"properties\":{}," +
232+
"\"enableMessaging\":false" +
233+
"}";
234+
235+
ApplicationDescriptor descriptor = mapper.readValue(json, ApplicationDescriptor.class);
236+
237+
assertEquals("app1", descriptor.getApplicationId());
238+
assertTrue(descriptor.getProperties().isEmpty());
239+
assertFalse(descriptor.isEnableMessaging());
240+
}
241+
242+
@Test
243+
void testDeserialize_withClasspathAndProperties() throws IOException {
244+
String json = "{" +
245+
"\"applicationId\":\"app1\"," +
246+
"\"name\":\"App\"," +
247+
"\"version\":\"1.0\"," +
248+
"\"mainClass\":\"com.App\"," +
249+
"\"classpath\":[\"file:///lib/a.jar\",\"file:///lib/b.jar\"]," +
250+
"\"properties\":{\"key1\":\"value1\",\"key2\":\"value2\"}," +
251+
"\"enableMessaging\":true" +
252+
"}";
253+
254+
ApplicationDescriptor descriptor = mapper.readValue(json, ApplicationDescriptor.class);
255+
256+
assertEquals("app1", descriptor.getApplicationId());
257+
assertEquals(2, descriptor.getClasspathEntries().size());
258+
assertEquals(2, descriptor.getProperties().size());
259+
assertEquals("value1", descriptor.getProperties().get("key1"));
260+
assertEquals("value2", descriptor.getProperties().get("key2"));
261+
assertTrue(descriptor.isEnableMessaging());
262+
}
263+
}

0 commit comments

Comments
 (0)