Dropwizard Jersey Support - Jersey integration module for the Dropwizard Java framework
—
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.
Message body writers and exception handling for Java 8 Optional types with automatic HTTP status code mapping.
/**
* Message body writer for Optional<?> types
* Returns 404 Not Found for empty Optional values
*/
public class OptionalMessageBodyWriter implements MessageBodyWriter<Optional<?>> {
/** Checks if this writer can handle the given Optional type */
public boolean isWriteable(Class<?> type, Type genericType,
Annotation[] annotations, MediaType mediaType);
/** Gets content length for Optional (returns -1 for chunked encoding) */
public long getSize(Optional<?> optional, Class<?> type, Type genericType,
Annotation[] annotations, MediaType mediaType);
/** Writes Optional content or throws EmptyOptionalException for empty values */
public void writeTo(Optional<?> optional, Class<?> type, Type genericType,
Annotation[] annotations, MediaType mediaType,
MultivaluedMap<String, Object> httpHeaders,
OutputStream entityStream) throws IOException, WebApplicationException;
}
/**
* Message body writer for OptionalInt primitive type
* Handles primitive int Optional values
*/
public class OptionalIntMessageBodyWriter implements MessageBodyWriter<OptionalInt> {
/** Checks if this writer can handle OptionalInt */
public boolean isWriteable(Class<?> type, Type genericType,
Annotation[] annotations, MediaType mediaType);
/** Writes OptionalInt value or throws exception for empty values */
public void writeTo(OptionalInt optionalInt, Class<?> type, Type genericType,
Annotation[] annotations, MediaType mediaType,
MultivaluedMap<String, Object> httpHeaders,
OutputStream entityStream) throws IOException, WebApplicationException;
}
/**
* Message body writer for OptionalLong primitive type
*/
public class OptionalLongMessageBodyWriter implements MessageBodyWriter<OptionalLong> {
public boolean isWriteable(Class<?> type, Type genericType,
Annotation[] annotations, MediaType mediaType);
public void writeTo(OptionalLong optionalLong, Class<?> type, Type genericType,
Annotation[] annotations, MediaType mediaType,
MultivaluedMap<String, Object> httpHeaders,
OutputStream entityStream) throws IOException, WebApplicationException;
}
/**
* Message body writer for OptionalDouble primitive type
*/
public class OptionalDoubleMessageBodyWriter implements MessageBodyWriter<OptionalDouble> {
public boolean isWriteable(Class<?> type, Type genericType,
Annotation[] annotations, MediaType mediaType);
public void writeTo(OptionalDouble optionalDouble, Class<?> type, Type genericType,
Annotation[] annotations, MediaType mediaType,
MultivaluedMap<String, Object> httpHeaders,
OutputStream entityStream) throws IOException, WebApplicationException;
}Usage Examples:
import java.util.Optional;
import java.util.OptionalInt;
import java.util.OptionalLong;
import java.util.OptionalDouble;
import jakarta.ws.rs.*;
@Path("/users")
public class UserResource {
@GET
@Path("/{id}")
public Optional<User> getUser(@PathParam("id") UUIDParam userId) {
// Returns 404 automatically if Optional is empty
return userService.findById(userId.get());
}
@GET
@Path("/{id}/age")
public OptionalInt getUserAge(@PathParam("id") UUIDParam userId) {
// Returns 404 if user not found or age not set
return userService.getUserAge(userId.get());
}
@GET
@Path("/{id}/salary")
public OptionalLong getUserSalary(@PathParam("id") UUIDParam userId) {
// Returns 404 if user not found or salary not set
return userService.getUserSalary(userId.get());
}
@GET
@Path("/{id}/rating")
public OptionalDouble getUserRating(@PathParam("id") UUIDParam userId) {
// Returns 404 if user not found or rating not set
return userService.getUserRating(userId.get());
}
}Exception types and mappers for handling empty Optional values with different HTTP response strategies.
/**
* Exception thrown when an Optional value is empty
* Used internally by Optional message body writers
*/
public class EmptyOptionalException extends WebApplicationException {
/** Creates exception with default message */
public EmptyOptionalException();
/** Creates exception with custom message */
public EmptyOptionalException(String message);
/** Creates exception with cause */
public EmptyOptionalException(Throwable cause);
/** Creates exception with message and cause */
public EmptyOptionalException(String message, Throwable cause);
}
/**
* Exception mapper that converts EmptyOptionalException to 404 Not Found
* Default behavior for empty Optional values
*/
public class EmptyOptionalExceptionMapper implements ExceptionMapper<EmptyOptionalException> {
/** Maps EmptyOptionalException to 404 Not Found response */
public Response toResponse(EmptyOptionalException exception);
}
/**
* Alternative exception mapper that converts EmptyOptionalException to 204 No Content
* Useful when empty values should return success with no content
*/
public class EmptyOptionalNoContentExceptionMapper implements ExceptionMapper<EmptyOptionalException> {
/** Maps EmptyOptionalException to 204 No Content response */
public Response toResponse(EmptyOptionalException exception);
}Support for Google Guava Optional types for legacy code compatibility.
/**
* Message body writer for Guava Optional types
* Provides compatibility with Google Guava Optional
*/
public class OptionalMessageBodyWriter implements MessageBodyWriter<com.google.common.base.Optional<?>> {
/** Checks if this writer can handle Guava Optional */
public boolean isWriteable(Class<?> type, Type genericType,
Annotation[] annotations, MediaType mediaType);
/** Writes Guava Optional content or returns 404 for absent values */
public void writeTo(com.google.common.base.Optional<?> optional, Class<?> type, Type genericType,
Annotation[] annotations, MediaType mediaType,
MultivaluedMap<String, Object> httpHeaders,
OutputStream entityStream) throws IOException, WebApplicationException;
}
/**
* Parameter binder for Guava Optional parameter converters
* Enables parameter conversion for Guava Optional types
*/
public class OptionalParamBinder extends AbstractBinder {
/** Configures Guava Optional parameter bindings */
protected void configure();
}
/**
* Parameter converter provider for Guava Optional types
* Provides automatic conversion of query/path parameters to Guava Optional
*/
public class OptionalParamConverterProvider implements ParamConverterProvider {
/** Gets parameter converter for Guava Optional types */
public <T> ParamConverter<T> getConverter(Class<T> rawType, Type genericType, Annotation[] annotations);
}Guava Optional Usage:
import com.google.common.base.Optional;
import jakarta.ws.rs.*;
@Path("/legacy")
public class LegacyResource {
@GET
@Path("/users/{id}")
public Optional<User> getUser(@PathParam("id") UUIDParam userId) {
// Using Guava Optional for legacy compatibility
User user = userService.findById(userId.get());
return Optional.fromNullable(user);
}
@GET
@Path("/search")
public List<User> searchUsers(@QueryParam("name") Optional<String> name,
@QueryParam("email") Optional<String> email) {
// Guava Optional parameters
String nameFilter = name.orNull();
String emailFilter = email.orNull();
return userService.search(nameFilter, emailFilter);
}
}@Path("/api")
public class OptionalResponseExamples {
@GET
@Path("/users/{id}")
public Optional<User> getUser(@PathParam("id") UUIDParam userId) {
// Service returns Optional - automatically converts to 404 if empty
return userService.findById(userId.get());
}
@GET
@Path("/users/{id}/profile")
public Optional<UserProfile> getUserProfile(@PathParam("id") UUIDParam userId) {
// Nested service calls with Optional chaining
return userService.findById(userId.get())
.flatMap(user -> profileService.getProfile(user.getId()));
}
@GET
@Path("/users/{id}/preferences")
public Optional<UserPreferences> getUserPreferences(@PathParam("id") UUIDParam userId) {
// Returns 404 if user not found or preferences not set
return userService.findById(userId.get())
.map(user -> user.getPreferences())
.filter(prefs -> prefs != null);
}
}@Path("/conditional")
public class ConditionalOptionalResource {
@GET
@Path("/data/{id}")
public Optional<Data> getData(@PathParam("id") UUIDParam dataId,
@QueryParam("includeDetails") boolean includeDetails) {
Optional<Data> data = dataService.findById(dataId.get());
if (includeDetails) {
// Include additional details if requested
return data.map(d -> dataService.enrichWithDetails(d));
}
return data;
}
@GET
@Path("/users/{id}/stats")
public Optional<UserStats> getUserStats(@PathParam("id") UUIDParam userId,
@QueryParam("period") String period) {
return userService.findById(userId.get())
.flatMap(user -> {
if ("month".equals(period)) {
return statsService.getMonthlyStats(user.getId());
} else if ("year".equals(period)) {
return statsService.getYearlyStats(user.getId());
} else {
return statsService.getAllTimeStats(user.getId());
}
});
}
}@Path("/custom")
public class CustomOptionalResource {
@GET
@Path("/data/{id}")
public Response getDataWithCustomStatus(@PathParam("id") UUIDParam dataId) {
Optional<Data> data = dataService.findById(dataId.get());
if (data.isPresent()) {
return Response.ok(data.get()).build();
} else {
// Custom response for empty Optional
return Response.status(204) // No Content instead of 404
.header("X-Reason", "Data not available")
.build();
}
}
@GET
@Path("/users/{id}/avatar")
public Response getUserAvatar(@PathParam("id") UUIDParam userId) {
Optional<byte[]> avatar = userService.getAvatar(userId.get());
if (avatar.isPresent()) {
return Response.ok(avatar.get())
.type("image/png")
.build();
} else {
// Redirect to default avatar instead of 404
return Response.seeOther(URI.create("/images/default-avatar.png")).build();
}
}
}public class OptionalConfiguration {
public void configureOptionalHandling(JerseyEnvironment jersey) {
// Use 204 No Content instead of 404 for empty Optionals
jersey.register(EmptyOptionalNoContentExceptionMapper.class);
// Or keep default 404 behavior
// jersey.register(EmptyOptionalExceptionMapper.class); // Default
// Register optional message body writers (automatically registered by DropwizardResourceConfig)
jersey.register(OptionalMessageBodyWriter.class);
jersey.register(OptionalIntMessageBodyWriter.class);
jersey.register(OptionalLongMessageBodyWriter.class);
jersey.register(OptionalDoubleMessageBodyWriter.class);
}
public void configureGuavaOptional(JerseyEnvironment jersey) {
// Enable Guava Optional support
jersey.register(io.dropwizard.jersey.guava.OptionalMessageBodyWriter.class);
jersey.register(new OptionalParamBinder());
}
}@Provider
public class CustomOptionalExceptionMapper implements ExceptionMapper<EmptyOptionalException> {
@Override
public Response toResponse(EmptyOptionalException exception) {
// Custom logic based on request context
UriInfo uriInfo = getUriInfo(); // Inject UriInfo
String path = uriInfo.getPath();
if (path.contains("/users/")) {
return Response.status(404)
.entity(new ErrorMessage(404, "User not found"))
.build();
} else if (path.contains("/data/")) {
return Response.status(204)
.header("X-Data-Status", "Not Available")
.build();
} else {
return Response.status(404)
.entity(new ErrorMessage(404, "Resource not found"))
.build();
}
}
}public class OptionalBestPractices {
// Good: Use Optional for single resource lookups that may not exist
@GET
@Path("/users/{id}")
public Optional<User> getUser(@PathParam("id") UUIDParam userId) {
return userService.findById(userId.get());
}
// Good: Use Optional for optional resource properties
@GET
@Path("/users/{id}/avatar")
public Optional<byte[]> getUserAvatar(@PathParam("id") UUIDParam userId) {
return userService.getAvatar(userId.get());
}
// Avoid: Don't use Optional for collections - return empty collections instead
@GET
@Path("/users")
public List<User> getUsers() {
return userService.getAllUsers(); // Returns empty list, not Optional<List<User>>
}
// Good: Use primitive Optionals for numeric values that may be absent
@GET
@Path("/users/{id}/age")
public OptionalInt getUserAge(@PathParam("id") UUIDParam userId) {
return userService.getUserAge(userId.get());
}
}@Path("/search")
public class OptionalParameterResource {
@GET
public List<User> searchUsers(@QueryParam("name") Optional<String> name,
@QueryParam("minAge") OptionalInt minAge,
@QueryParam("maxAge") OptionalInt maxAge) {
SearchCriteria criteria = new SearchCriteria();
// Handle optional parameters
name.ifPresent(criteria::setName);
minAge.ifPresent(criteria::setMinAge);
maxAge.ifPresent(criteria::setMaxAge);
return userService.search(criteria);
}
// Alternative approach with null checks
@GET
@Path("/alt")
public List<User> searchUsersAlt(@QueryParam("name") String name,
@QueryParam("minAge") Integer minAge,
@QueryParam("maxAge") Integer maxAge) {
SearchCriteria criteria = new SearchCriteria();
if (name != null) criteria.setName(name);
if (minAge != null) criteria.setMinAge(minAge);
if (maxAge != null) criteria.setMaxAge(maxAge);
return userService.search(criteria);
}
}public class OptionalChainingExamples {
@GET
@Path("/users/{id}/organization/name")
public Optional<String> getUserOrganizationName(@PathParam("id") UUIDParam userId) {
// Chain optional operations
return userService.findById(userId.get())
.flatMap(user -> organizationService.findById(user.getOrganizationId()))
.map(org -> org.getName());
}
@GET
@Path("/orders/{id}/customer/email")
public Optional<String> getOrderCustomerEmail(@PathParam("id") UUIDParam orderId) {
return orderService.findById(orderId.get())
.flatMap(order -> customerService.findById(order.getCustomerId()))
.map(customer -> customer.getEmail())
.filter(email -> email != null && !email.isEmpty());
}
}Install with Tessl CLI
npx tessl i tessl/maven-io-dropwizard--dropwizard-jersey