0
# Optional Handling
1
2
Support for Java 8 Optional and Guava Optional types with automatic serialization and 404 responses for empty values. Provides seamless integration between optional types and HTTP semantics in JAX-RS resources.
3
4
## Capabilities
5
6
### Java 8 Optional Support
7
8
Message body writers and exception handling for Java 8 Optional types with automatic HTTP status code mapping.
9
10
```java { .api }
11
/**
12
* Message body writer for Optional<?> types
13
* Returns 404 Not Found for empty Optional values
14
*/
15
public class OptionalMessageBodyWriter implements MessageBodyWriter<Optional<?>> {
16
17
/** Checks if this writer can handle the given Optional type */
18
public boolean isWriteable(Class<?> type, Type genericType,
19
Annotation[] annotations, MediaType mediaType);
20
21
/** Gets content length for Optional (returns -1 for chunked encoding) */
22
public long getSize(Optional<?> optional, Class<?> type, Type genericType,
23
Annotation[] annotations, MediaType mediaType);
24
25
/** Writes Optional content or throws EmptyOptionalException for empty values */
26
public void writeTo(Optional<?> optional, Class<?> type, Type genericType,
27
Annotation[] annotations, MediaType mediaType,
28
MultivaluedMap<String, Object> httpHeaders,
29
OutputStream entityStream) throws IOException, WebApplicationException;
30
}
31
32
/**
33
* Message body writer for OptionalInt primitive type
34
* Handles primitive int Optional values
35
*/
36
public class OptionalIntMessageBodyWriter implements MessageBodyWriter<OptionalInt> {
37
38
/** Checks if this writer can handle OptionalInt */
39
public boolean isWriteable(Class<?> type, Type genericType,
40
Annotation[] annotations, MediaType mediaType);
41
42
/** Writes OptionalInt value or throws exception for empty values */
43
public void writeTo(OptionalInt optionalInt, Class<?> type, Type genericType,
44
Annotation[] annotations, MediaType mediaType,
45
MultivaluedMap<String, Object> httpHeaders,
46
OutputStream entityStream) throws IOException, WebApplicationException;
47
}
48
49
/**
50
* Message body writer for OptionalLong primitive type
51
*/
52
public class OptionalLongMessageBodyWriter implements MessageBodyWriter<OptionalLong> {
53
54
public boolean isWriteable(Class<?> type, Type genericType,
55
Annotation[] annotations, MediaType mediaType);
56
57
public void writeTo(OptionalLong optionalLong, Class<?> type, Type genericType,
58
Annotation[] annotations, MediaType mediaType,
59
MultivaluedMap<String, Object> httpHeaders,
60
OutputStream entityStream) throws IOException, WebApplicationException;
61
}
62
63
/**
64
* Message body writer for OptionalDouble primitive type
65
*/
66
public class OptionalDoubleMessageBodyWriter implements MessageBodyWriter<OptionalDouble> {
67
68
public boolean isWriteable(Class<?> type, Type genericType,
69
Annotation[] annotations, MediaType mediaType);
70
71
public void writeTo(OptionalDouble optionalDouble, Class<?> type, Type genericType,
72
Annotation[] annotations, MediaType mediaType,
73
MultivaluedMap<String, Object> httpHeaders,
74
OutputStream entityStream) throws IOException, WebApplicationException;
75
}
76
```
77
78
**Usage Examples:**
79
80
```java
81
import java.util.Optional;
82
import java.util.OptionalInt;
83
import java.util.OptionalLong;
84
import java.util.OptionalDouble;
85
import jakarta.ws.rs.*;
86
87
@Path("/users")
88
public class UserResource {
89
90
@GET
91
@Path("/{id}")
92
public Optional<User> getUser(@PathParam("id") UUIDParam userId) {
93
// Returns 404 automatically if Optional is empty
94
return userService.findById(userId.get());
95
}
96
97
@GET
98
@Path("/{id}/age")
99
public OptionalInt getUserAge(@PathParam("id") UUIDParam userId) {
100
// Returns 404 if user not found or age not set
101
return userService.getUserAge(userId.get());
102
}
103
104
@GET
105
@Path("/{id}/salary")
106
public OptionalLong getUserSalary(@PathParam("id") UUIDParam userId) {
107
// Returns 404 if user not found or salary not set
108
return userService.getUserSalary(userId.get());
109
}
110
111
@GET
112
@Path("/{id}/rating")
113
public OptionalDouble getUserRating(@PathParam("id") UUIDParam userId) {
114
// Returns 404 if user not found or rating not set
115
return userService.getUserRating(userId.get());
116
}
117
}
118
```
119
120
### Empty Optional Exception Handling
121
122
Exception types and mappers for handling empty Optional values with different HTTP response strategies.
123
124
```java { .api }
125
/**
126
* Exception thrown when an Optional value is empty
127
* Used internally by Optional message body writers
128
*/
129
public class EmptyOptionalException extends WebApplicationException {
130
131
/** Creates exception with default message */
132
public EmptyOptionalException();
133
134
/** Creates exception with custom message */
135
public EmptyOptionalException(String message);
136
137
/** Creates exception with cause */
138
public EmptyOptionalException(Throwable cause);
139
140
/** Creates exception with message and cause */
141
public EmptyOptionalException(String message, Throwable cause);
142
}
143
144
/**
145
* Exception mapper that converts EmptyOptionalException to 404 Not Found
146
* Default behavior for empty Optional values
147
*/
148
public class EmptyOptionalExceptionMapper implements ExceptionMapper<EmptyOptionalException> {
149
150
/** Maps EmptyOptionalException to 404 Not Found response */
151
public Response toResponse(EmptyOptionalException exception);
152
}
153
154
/**
155
* Alternative exception mapper that converts EmptyOptionalException to 204 No Content
156
* Useful when empty values should return success with no content
157
*/
158
public class EmptyOptionalNoContentExceptionMapper implements ExceptionMapper<EmptyOptionalException> {
159
160
/** Maps EmptyOptionalException to 204 No Content response */
161
public Response toResponse(EmptyOptionalException exception);
162
}
163
```
164
165
### Guava Optional Support
166
167
Support for Google Guava Optional types for legacy code compatibility.
168
169
```java { .api }
170
/**
171
* Message body writer for Guava Optional types
172
* Provides compatibility with Google Guava Optional
173
*/
174
public class OptionalMessageBodyWriter implements MessageBodyWriter<com.google.common.base.Optional<?>> {
175
176
/** Checks if this writer can handle Guava Optional */
177
public boolean isWriteable(Class<?> type, Type genericType,
178
Annotation[] annotations, MediaType mediaType);
179
180
/** Writes Guava Optional content or returns 404 for absent values */
181
public void writeTo(com.google.common.base.Optional<?> optional, Class<?> type, Type genericType,
182
Annotation[] annotations, MediaType mediaType,
183
MultivaluedMap<String, Object> httpHeaders,
184
OutputStream entityStream) throws IOException, WebApplicationException;
185
}
186
187
/**
188
* Parameter binder for Guava Optional parameter converters
189
* Enables parameter conversion for Guava Optional types
190
*/
191
public class OptionalParamBinder extends AbstractBinder {
192
193
/** Configures Guava Optional parameter bindings */
194
protected void configure();
195
}
196
197
/**
198
* Parameter converter provider for Guava Optional types
199
* Provides automatic conversion of query/path parameters to Guava Optional
200
*/
201
public class OptionalParamConverterProvider implements ParamConverterProvider {
202
203
/** Gets parameter converter for Guava Optional types */
204
public <T> ParamConverter<T> getConverter(Class<T> rawType, Type genericType, Annotation[] annotations);
205
}
206
```
207
208
**Guava Optional Usage:**
209
210
```java
211
import com.google.common.base.Optional;
212
import jakarta.ws.rs.*;
213
214
@Path("/legacy")
215
public class LegacyResource {
216
217
@GET
218
@Path("/users/{id}")
219
public Optional<User> getUser(@PathParam("id") UUIDParam userId) {
220
// Using Guava Optional for legacy compatibility
221
User user = userService.findById(userId.get());
222
return Optional.fromNullable(user);
223
}
224
225
@GET
226
@Path("/search")
227
public List<User> searchUsers(@QueryParam("name") Optional<String> name,
228
@QueryParam("email") Optional<String> email) {
229
// Guava Optional parameters
230
String nameFilter = name.orNull();
231
String emailFilter = email.orNull();
232
return userService.search(nameFilter, emailFilter);
233
}
234
}
235
```
236
237
## Optional Response Patterns
238
239
### Standard Optional Usage
240
241
```java
242
@Path("/api")
243
public class OptionalResponseExamples {
244
245
@GET
246
@Path("/users/{id}")
247
public Optional<User> getUser(@PathParam("id") UUIDParam userId) {
248
// Service returns Optional - automatically converts to 404 if empty
249
return userService.findById(userId.get());
250
}
251
252
@GET
253
@Path("/users/{id}/profile")
254
public Optional<UserProfile> getUserProfile(@PathParam("id") UUIDParam userId) {
255
// Nested service calls with Optional chaining
256
return userService.findById(userId.get())
257
.flatMap(user -> profileService.getProfile(user.getId()));
258
}
259
260
@GET
261
@Path("/users/{id}/preferences")
262
public Optional<UserPreferences> getUserPreferences(@PathParam("id") UUIDParam userId) {
263
// Returns 404 if user not found or preferences not set
264
return userService.findById(userId.get())
265
.map(user -> user.getPreferences())
266
.filter(prefs -> prefs != null);
267
}
268
}
269
```
270
271
### Conditional Optional Responses
272
273
```java
274
@Path("/conditional")
275
public class ConditionalOptionalResource {
276
277
@GET
278
@Path("/data/{id}")
279
public Optional<Data> getData(@PathParam("id") UUIDParam dataId,
280
@QueryParam("includeDetails") boolean includeDetails) {
281
Optional<Data> data = dataService.findById(dataId.get());
282
283
if (includeDetails) {
284
// Include additional details if requested
285
return data.map(d -> dataService.enrichWithDetails(d));
286
}
287
288
return data;
289
}
290
291
@GET
292
@Path("/users/{id}/stats")
293
public Optional<UserStats> getUserStats(@PathParam("id") UUIDParam userId,
294
@QueryParam("period") String period) {
295
return userService.findById(userId.get())
296
.flatMap(user -> {
297
if ("month".equals(period)) {
298
return statsService.getMonthlyStats(user.getId());
299
} else if ("year".equals(period)) {
300
return statsService.getYearlyStats(user.getId());
301
} else {
302
return statsService.getAllTimeStats(user.getId());
303
}
304
});
305
}
306
}
307
```
308
309
### Optional with Custom HTTP Status
310
311
```java
312
@Path("/custom")
313
public class CustomOptionalResource {
314
315
@GET
316
@Path("/data/{id}")
317
public Response getDataWithCustomStatus(@PathParam("id") UUIDParam dataId) {
318
Optional<Data> data = dataService.findById(dataId.get());
319
320
if (data.isPresent()) {
321
return Response.ok(data.get()).build();
322
} else {
323
// Custom response for empty Optional
324
return Response.status(204) // No Content instead of 404
325
.header("X-Reason", "Data not available")
326
.build();
327
}
328
}
329
330
@GET
331
@Path("/users/{id}/avatar")
332
public Response getUserAvatar(@PathParam("id") UUIDParam userId) {
333
Optional<byte[]> avatar = userService.getAvatar(userId.get());
334
335
if (avatar.isPresent()) {
336
return Response.ok(avatar.get())
337
.type("image/png")
338
.build();
339
} else {
340
// Redirect to default avatar instead of 404
341
return Response.seeOther(URI.create("/images/default-avatar.png")).build();
342
}
343
}
344
}
345
```
346
347
## Configuration and Customization
348
349
### Configuring Optional Behavior
350
351
```java
352
public class OptionalConfiguration {
353
354
public void configureOptionalHandling(JerseyEnvironment jersey) {
355
// Use 204 No Content instead of 404 for empty Optionals
356
jersey.register(EmptyOptionalNoContentExceptionMapper.class);
357
358
// Or keep default 404 behavior
359
// jersey.register(EmptyOptionalExceptionMapper.class); // Default
360
361
// Register optional message body writers (automatically registered by DropwizardResourceConfig)
362
jersey.register(OptionalMessageBodyWriter.class);
363
jersey.register(OptionalIntMessageBodyWriter.class);
364
jersey.register(OptionalLongMessageBodyWriter.class);
365
jersey.register(OptionalDoubleMessageBodyWriter.class);
366
}
367
368
public void configureGuavaOptional(JerseyEnvironment jersey) {
369
// Enable Guava Optional support
370
jersey.register(io.dropwizard.jersey.guava.OptionalMessageBodyWriter.class);
371
jersey.register(new OptionalParamBinder());
372
}
373
}
374
```
375
376
### Custom Optional Exception Mapper
377
378
```java
379
@Provider
380
public class CustomOptionalExceptionMapper implements ExceptionMapper<EmptyOptionalException> {
381
382
@Override
383
public Response toResponse(EmptyOptionalException exception) {
384
// Custom logic based on request context
385
UriInfo uriInfo = getUriInfo(); // Inject UriInfo
386
String path = uriInfo.getPath();
387
388
if (path.contains("/users/")) {
389
return Response.status(404)
390
.entity(new ErrorMessage(404, "User not found"))
391
.build();
392
} else if (path.contains("/data/")) {
393
return Response.status(204)
394
.header("X-Data-Status", "Not Available")
395
.build();
396
} else {
397
return Response.status(404)
398
.entity(new ErrorMessage(404, "Resource not found"))
399
.build();
400
}
401
}
402
}
403
```
404
405
## Best Practices
406
407
### When to Use Optional
408
409
```java
410
public class OptionalBestPractices {
411
412
// Good: Use Optional for single resource lookups that may not exist
413
@GET
414
@Path("/users/{id}")
415
public Optional<User> getUser(@PathParam("id") UUIDParam userId) {
416
return userService.findById(userId.get());
417
}
418
419
// Good: Use Optional for optional resource properties
420
@GET
421
@Path("/users/{id}/avatar")
422
public Optional<byte[]> getUserAvatar(@PathParam("id") UUIDParam userId) {
423
return userService.getAvatar(userId.get());
424
}
425
426
// Avoid: Don't use Optional for collections - return empty collections instead
427
@GET
428
@Path("/users")
429
public List<User> getUsers() {
430
return userService.getAllUsers(); // Returns empty list, not Optional<List<User>>
431
}
432
433
// Good: Use primitive Optionals for numeric values that may be absent
434
@GET
435
@Path("/users/{id}/age")
436
public OptionalInt getUserAge(@PathParam("id") UUIDParam userId) {
437
return userService.getUserAge(userId.get());
438
}
439
}
440
```
441
442
### Optional Parameter Handling
443
444
```java
445
@Path("/search")
446
public class OptionalParameterResource {
447
448
@GET
449
public List<User> searchUsers(@QueryParam("name") Optional<String> name,
450
@QueryParam("minAge") OptionalInt minAge,
451
@QueryParam("maxAge") OptionalInt maxAge) {
452
453
SearchCriteria criteria = new SearchCriteria();
454
455
// Handle optional parameters
456
name.ifPresent(criteria::setName);
457
minAge.ifPresent(criteria::setMinAge);
458
maxAge.ifPresent(criteria::setMaxAge);
459
460
return userService.search(criteria);
461
}
462
463
// Alternative approach with null checks
464
@GET
465
@Path("/alt")
466
public List<User> searchUsersAlt(@QueryParam("name") String name,
467
@QueryParam("minAge") Integer minAge,
468
@QueryParam("maxAge") Integer maxAge) {
469
470
SearchCriteria criteria = new SearchCriteria();
471
472
if (name != null) criteria.setName(name);
473
if (minAge != null) criteria.setMinAge(minAge);
474
if (maxAge != null) criteria.setMaxAge(maxAge);
475
476
return userService.search(criteria);
477
}
478
}
479
```
480
481
### Optional Chaining
482
483
```java
484
public class OptionalChainingExamples {
485
486
@GET
487
@Path("/users/{id}/organization/name")
488
public Optional<String> getUserOrganizationName(@PathParam("id") UUIDParam userId) {
489
// Chain optional operations
490
return userService.findById(userId.get())
491
.flatMap(user -> organizationService.findById(user.getOrganizationId()))
492
.map(org -> org.getName());
493
}
494
495
@GET
496
@Path("/orders/{id}/customer/email")
497
public Optional<String> getOrderCustomerEmail(@PathParam("id") UUIDParam orderId) {
498
return orderService.findById(orderId.get())
499
.flatMap(order -> customerService.findById(order.getCustomerId()))
500
.map(customer -> customer.getEmail())
501
.filter(email -> email != null && !email.isEmpty());
502
}
503
}
504
```