org.kiwiproject.jaxrs.KiwiResources Maven / Gradle / Ivy
Show all versions of kiwi Show documentation
package org.kiwiproject.jaxrs;
import static java.util.Objects.isNull;
import static java.util.Objects.nonNull;
import static org.kiwiproject.base.KiwiPreconditions.checkArgumentNotBlank;
import static org.kiwiproject.base.KiwiPreconditions.checkArgumentNotNull;
import static org.kiwiproject.base.KiwiStrings.f;
import static org.kiwiproject.collect.KiwiLists.first;
import static org.kiwiproject.collect.KiwiLists.isNullOrEmpty;
import static org.kiwiproject.jaxrs.KiwiJaxrsValidations.assertNotNull;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.primitives.Ints;
import com.google.common.primitives.Longs;
import jakarta.ws.rs.core.MultivaluedMap;
import jakarta.ws.rs.core.Response;
import lombok.experimental.UtilityClass;
import lombok.extern.slf4j.Slf4j;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.kiwiproject.base.KiwiStrings;
import org.kiwiproject.jaxrs.exception.JaxrsBadRequestException;
import org.kiwiproject.jaxrs.exception.JaxrsNotFoundException;
import java.net.URI;
import java.util.List;
import java.util.Map;
import java.util.Optional;
/**
* Static utilities for use in Jakarta REST resource classes. Contains utilities for verifying entities (e.g., obtained
* from a service or data access class), factories for creating new responses, and for validating query parameters.
*
* @apiNote Some methods in this class accept {@link Optional} arguments, which we know is considered a code smell
* by various people and analysis tools such as IntelliJ's inspections, Sonar, etc. However, we also like to return
* {@link Optional} from data access code (e.g. a DAO "findById" method where the object might not exist if it was
* recently deleted). In such cases, we can simply take the Optional returned by those finder methods and pass them
* directly to the utilities provided here without needing to call additional methods, for example, without needing to
* call {@code orElse(null)}. So, we acknowledge that it is generally not good to accept {@link Optional} arguments,
* but we're trading off convenience in this class against "generally accepted" practice.
* @see KiwiResponses
* @see KiwiStandardResponses
*/
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
@UtilityClass
@Slf4j
public class KiwiResources {
private static final Map EMPTY_HEADERS = Map.of();
private static final String PARAMETERS_MUST_NOT_BE_NULL = "parameters must not be null";
private static final String PARAMETER_NAME_MUST_NOT_BE_BLANK = "parameterName must not be blank";
/**
* Verifies that {@code resourceEntity} is not null, otherwise throws a {@link JaxrsNotFoundException}.
*
* @param resourceEntity the resource entity to verify
* @param the object type
* @throws JaxrsNotFoundException if the entity is null
*/
public static void verifyExistence(T resourceEntity) {
verifyExistence(resourceEntity, null);
}
/**
* Verifies that {@code resourceEntity} is not null and returns it,
* otherwise throws a {@link JaxrsNotFoundException}.
*
* @param resourceEntity the resource entity to verify
* @param the object type
* @return the entity, if it is not null
* @throws JaxrsNotFoundException if the entity is null
*/
@NonNull
public static T verifyExistenceAndReturn(T resourceEntity) {
return verifyExistence(Optional.ofNullable(resourceEntity));
}
/**
* Verifies that {@code resourceEntity} contains a value, otherwise throws a {@link JaxrsNotFoundException}.
*
* @param resourceEntity the resource entity to verify
* @param the object type
* @return the entity if the Optional contains a value
* @throws JaxrsNotFoundException if the entity is empty
*/
@NonNull
public static T verifyExistence(Optional resourceEntity) {
verifyExistence(resourceEntity.orElse(null), null);
return resourceEntity.orElseThrow();
}
/**
* Verifies that {@code resourceEntity} is not null, otherwise throws a {@link JaxrsNotFoundException}.
*
* @param resourceEntity the resource entity to verify
* @param entityType a Class representing the entity type, used in error messages
* @param identifier the unique identifier of the resource identity
* @param the object type
* @throws JaxrsNotFoundException if the entity is null
*/
public static void verifyExistence(T resourceEntity, Class entityType, Object identifier) {
var notFoundMessage = JaxrsNotFoundException.buildMessage(entityType.getSimpleName(), identifier);
verifyExistence(resourceEntity, notFoundMessage);
}
/**
* Verifies that {@code resourceEntity} is not null and returns it,
* otherwise throws a {@link JaxrsNotFoundException}.
*
* @param resourceEntity the resource entity to verify
* @param entityType a Class representing the entity type, used in error messages
* @param identifier the unique identifier of the resource identity
* @param the object type
* @return the entity, if it is not null
* @throws JaxrsNotFoundException if the entity is null
*/
@NonNull
public static T verifyExistenceAndReturn(T resourceEntity, Class entityType, Object identifier) {
return verifyExistence(Optional.ofNullable(resourceEntity), entityType, identifier);
}
/**
* Verifies that {@code resourceEntity} contains a value, otherwise throws {@link JaxrsNotFoundException}.
*
* @param resourceEntity the resource entity to verify
* @param entityType a Class representing the entity type, used in error messages
* @param identifier the unique identifier of the resource identity
* @param the object type
* @return the entity if the Optional contains a value
* @throws JaxrsNotFoundException if the entity is empty
*/
@NonNull
public static T verifyExistence(Optional resourceEntity, Class entityType, Object identifier) {
var notFoundMessage = JaxrsNotFoundException.buildMessage(entityType.getSimpleName(), identifier);
verifyExistence(resourceEntity.orElse(null), notFoundMessage);
return resourceEntity.orElseThrow();
}
/**
* Verifies that {@code resourceEntity} is not null, otherwise throws {@link JaxrsNotFoundException}.
*
* @param resourceEntity the resource entity to verify
* @param notFoundMessage the error message to include in the response entity
* @param the object type
* @throws JaxrsNotFoundException if the entity is null
*/
public static void verifyExistence(T resourceEntity, String notFoundMessage) {
if (isNull(resourceEntity)) {
throw new JaxrsNotFoundException(notFoundMessage);
}
}
/**
* Verifies that {@code resourceEntity} is not null and returns it,
* otherwise throws {@link JaxrsNotFoundException}.
*
* @param resourceEntity the resource entity to verify
* @param notFoundMessage the error message to include in the response entity
* @param the object type
* @return the entity, if it is not null
* @throws JaxrsNotFoundException if the entity is null
*/
@NonNull
public static T verifyExistenceAndReturn(T resourceEntity, String notFoundMessage) {
return verifyExistence(Optional.ofNullable(resourceEntity), notFoundMessage);
}
/**
* Verifies that {@code resourceEntity} contains a value, otherwise throws {@link JaxrsNotFoundException}.
*
* @param resourceEntity the resource entity to verify
* @param notFoundMessage the error message to include in the response entity
* @param the object type
* @return the entity if the Optional contains a value
* @throws JaxrsNotFoundException if the entity is empty
*/
@NonNull
public static T verifyExistence(Optional resourceEntity, String notFoundMessage) {
verifyExistence(resourceEntity.orElse(null), notFoundMessage);
return resourceEntity.orElseThrow();
}
/**
* Verifies that {@code resourceEntity} is not null, otherwise throws {@link JaxrsNotFoundException}.
*
* @param resourceEntity the resource entity to verify
* @param notFoundMessageTemplate template for the error message to include in the response entity; uses
* {@link KiwiStrings#format(String, Object...) KiwiStrings.format}
* to construct the message
* @param args the arguments to be substituted into the message template
* @param the object type
* @throws JaxrsNotFoundException if the entity is null
*/
public static void verifyExistence(T resourceEntity, String notFoundMessageTemplate, Object... args) {
if (isNull(resourceEntity)) {
var message = f(notFoundMessageTemplate, args);
throw new JaxrsNotFoundException(message);
}
}
/**
* Verifies that {@code resourceEntity} is not null and returns it,
* otherwise throws {@link JaxrsNotFoundException}.
*
* @param resourceEntity the resource entity to verify
* @param notFoundMessageTemplate template for the error message to include in the response entity; uses
* {@link KiwiStrings#format(String, Object...) KiwiStrings.format}
* to construct the message
* @param args the arguments to be substituted into the message template
* @param the object type
* @return the entity, if it is not null
* @throws JaxrsNotFoundException if the entity is empty
*/
@NonNull
public static T verifyExistenceAndReturn(T resourceEntity, String notFoundMessageTemplate, Object... args) {
return verifyExistence(Optional.ofNullable(resourceEntity), notFoundMessageTemplate, args);
}
/**
* Verifies that {@code resourceEntity} contains a value, otherwise throws {@link JaxrsNotFoundException}.
*
* @param resourceEntity the resource entity to verify
* @param notFoundMessageTemplate template for the error message to include in the response entity; uses
* {@link KiwiStrings#format(String, Object...) KiwiStrings.format}
* to construct the message
* @param args the arguments to be substituted into the message template
* @param the object type
* @return the entity if the Optional contains a value
* @throws JaxrsNotFoundException if the entity is empty
*/
@NonNull
public static T verifyExistence(Optional resourceEntity, String notFoundMessageTemplate, Object... args) {
verifyExistence(resourceEntity.orElse(null), notFoundMessageTemplate, args);
return resourceEntity.orElseThrow();
}
/**
* Builds a {@link Response} having the given status and entity.
*
* @param status the response status
* @param entity the response entity
* @return a response
*/
public static Response newResponse(Response.Status status, Object entity) {
return newResponseBuilder(status, entity).build();
}
/**
* Creates a {@link Response.ResponseBuilder} having the given status and entity.
* You can further modify the returned build, e.g., add custom headers, set cookies. Etc.
*
* @param status the response status
* @param entity the response entity
* @return a response builder
*/
public static Response.ResponseBuilder newResponseBuilder(Response.Status status, Object entity) {
return newResponseBuilder(status, entity, EMPTY_HEADERS);
}
/**
* Builds a {@link Response} having the given status, entity, and content type.
*
* @param status the response status
* @param entity the response entity
* @param contentType the value for the Content-Type header
* @return a response
*/
public static Response newResponse(Response.Status status,
Object entity,
String contentType) {
return newResponseBuilder(status, entity, contentType).build();
}
/**
* Creates a {@link Response.ResponseBuilder} having the given status, entity, and content type.
* You can further modify the returned build, e.g., add custom headers, set cookies. Etc.
*
* @param status the response status
* @param entity the response entity
* @param contentType the value for the Content-Type header
* @return a response builder
*/
public static Response.ResponseBuilder newResponseBuilder(Response.Status status,
Object entity,
String contentType) {
return newResponseBuilder(status, entity).type(contentType);
}
/**
* Builds a {@link Response} having the given status, entity, and (single-valued) headers.
*
* @param status the response status
* @param entity the response entity
* @param singleValuedHeaders map containing single-valued response headers
* @return a response
*/
public static Response newResponse(Response.Status status,
Object entity,
Map singleValuedHeaders) {
return newResponseBuilder(status, entity, singleValuedHeaders).build();
}
/**
* Creates a {@link Response.ResponseBuilder} having the given status, entity, and (single-valued) headers.
* You can further modify the returned build, e.g., add custom headers, set cookies. Etc.
*
* @param status the response status
* @param entity the response entity
* @param singleValuedHeaders map containing single-valued response headers
* @return a response builder
*/
public static Response.ResponseBuilder newResponseBuilder(Response.Status status,
Object entity,
Map singleValuedHeaders) {
var responseBuilder = Response.status(status).entity(entity);
singleValuedHeaders.forEach(responseBuilder::header);
return responseBuilder;
}
/**
* Builds a {@link Response} having the given status, entity, and headers.
*
* @param status the response status
* @param entity the response entity
* @param headers map containing response headers
* @return a response
*/
public static Response newResponse(Response.Status status,
Object entity,
MultivaluedMap headers) {
return newResponseBuilder(status, entity, headers).build();
}
/**
* Creates a {@link Response.ResponseBuilder} having the given status, entity, and headers.
* You can further modify the returned build, e.g., add custom headers, set cookies. Etc.
*
* @param status the response status
* @param entity the response entity
* @param headers map containing response headers
* @return a response builder
*/
public static Response.ResponseBuilder newResponseBuilder(Response.Status status,
Object entity,
MultivaluedMap headers) {
var responseBuilder = Response.status(status).entity(entity);
headers.forEach((name, values) ->
values.forEach(value -> responseBuilder.header(name, value)));
return responseBuilder;
}
/**
* Builds a {@link Response} with 201 Created status and a specified Location header and entity.
*
* @param location the value for the Location header
* @param entity the response entity
* @return a 202 Created response
*/
public static Response createdResponse(URI location, Object entity) {
return createdResponseBuilder(location, entity).build();
}
/**
* Creates a {@link Response.ResponseBuilder} having 201 Created status and a specified Location header and entity.
* You can further modify the returned build, e.g., add custom headers, set cookies. Etc.
*
* @param location the value for the Location header
* @param entity the response entity
* @return a 201 Created response builder
*/
public static Response.ResponseBuilder createdResponseBuilder(URI location, Object entity) {
return Response.created(location).entity(entity);
}
/**
* Builds a {@link Response} with {@code 200 OK} status and a specified entity.
*
* @param entity the response entity
* @return a {@code 200 OK} response
*/
public static Response okResponse(Object entity) {
return okResponseBuilder(entity).build();
}
/**
* Creates a {@link Response.ResponseBuilder} having {@code 200 OK} status and a specified entity.
* You can further modify the returned build, e.g., add custom headers, set cookies. Etc.
*
* @param entity the response entity
* @return a {@code 200 OK} response builder
*/
public static Response.ResponseBuilder okResponseBuilder(Object entity) {
return Response.ok(entity);
}
/**
* Convenience wrapper around {@link Response#fromResponse(Response)} that also buffers the response entity by
* calling {@link Response#bufferEntity()} on the given response. This returns a {@link Response} instead of a
* response builder.
*
* NOTE: The reason this method exists is due to the note in the Javadoc of {@link Response#fromResponse(Response)}
* which states: "Note that if the entity is backed by an un-consumed input stream, the reference to the stream
* is copied. In such case make sure to buffer the entity stream of the original response instance before passing
* it to this method." So, rather than having the same boilerplate code in various locations (or as we've
* seen many times, people forgetting to buffer the response entity), this provides a single method to perform
* the same logic and ensure the entity is buffered.
*
* @param originalResponse a Response from which the status code, entity and response headers will be copied.
* @return a new response
* @see Response#fromResponse(Response)
* @see Response#bufferEntity()
*/
public static Response newResponseBufferingEntityFrom(Response originalResponse) {
return newResponseBuilderBufferingEntityFrom(originalResponse).build();
}
/**
* Convenience wrapper around {@link Response#fromResponse(Response)} that also buffers the response entity by
* calling {@link Response#bufferEntity()} on the given response.
*
* NOTE: The reason this method exists is due to the note in the Javadoc of {@link Response#fromResponse(Response)}
* which states: "Note that if the entity is backed by an un-consumed input stream, the reference to the stream
* is copied. In such case make sure to buffer the entity stream of the original response instance before passing
* it to this method." So, rather than having the same boilerplate code in various locations (or as we've
* seen many times, people forgetting to buffer the response entity), this provides a single method to perform
* the same logic and ensure the entity is buffered.
*
* @param originalResponse a Response from which the status code, entity and response headers will be copied.
* @return a new response builder
* @see Response#fromResponse(Response)
* @see Response#bufferEntity()
*/
public static Response.ResponseBuilder newResponseBuilderBufferingEntityFrom(Response originalResponse) {
var wasBuffered = originalResponse.bufferEntity();
if (!wasBuffered) {
LOG.warn("Attempt to buffer entity in original response returned false; possible causes:" +
" it was not backed by an unconsumed input stream; the input stream was already consumed; or, it did not have an entity");
}
return Response.fromResponse(originalResponse);
}
/**
* Checks whether {@code parameters} contains parameter named {@code parameterName} that is an integer or
* something that can be converted into an integer.
*
* @param parameters the parameters to check
* @param parameterName name of the parameter which should be present
* @param the type of values in the map
* @return the int value of the validated parameter
* @throws JaxrsBadRequestException if the specified parameter is not present, or is not an integer
*/
public static int validateIntParameter(Map parameters, String parameterName) {
checkArgumentNotNull(parameters, PARAMETERS_MUST_NOT_BE_NULL);
checkArgumentNotBlank(parameterName, PARAMETER_NAME_MUST_NOT_BE_BLANK);
var value = parameters.get(parameterName);
assertNotNull(parameterName, value);
return parseIntOrThrowBadRequest(value, parameterName);
}
/**
* Checks whether {@code parameters} contains a parameter named {@code parameterName} that has at least one
* value that can be converted into an integer. If there is more than one value, then the first one returned
* by {@link MultivaluedMap#getFirst(Object)} is returned.
*
* @param parameters the multivalued parameters to check
* @param parameterName name of the parameter which should be present
* @param the type of values in the multivalued map
* @return the int value of the validated parameter
* @throws JaxrsBadRequestException if the specified parameter is not present with at least one value or is
* not an integer
*/
public static int validateOneIntParameter(MultivaluedMap parameters,
String parameterName) {
checkArgumentNotNull(parameters, PARAMETERS_MUST_NOT_BE_NULL);
checkArgumentNotBlank(parameterName, PARAMETER_NAME_MUST_NOT_BE_BLANK);
var value = parameters.getFirst(parameterName);
assertNotNull(parameterName, value);
return parseIntOrThrowBadRequest(value, parameterName);
}
/**
* Checks whether {@code parameters} contains a parameter named {@code parameterName} that has exactly one
* value that can be converted into an integer. If there is more than one value, this is considered a bad request
* and a {@link JaxrsBadRequestException} is thrown.
*
* @param parameters the multivalued parameters to check
* @param parameterName name of the parameter which should be present
* @param the type of values in the multivalued map
* @return the int value of the validated parameter
* @throws JaxrsBadRequestException if the specified parameter is not present with only one value or is
* not an integer
*/
public static int validateExactlyOneIntParameter(MultivaluedMap parameters,
String parameterName) {
checkArgumentNotNull(parameters, PARAMETERS_MUST_NOT_BE_NULL);
checkArgumentNotBlank(parameterName, PARAMETER_NAME_MUST_NOT_BE_BLANK);
var values = parameters.get(parameterName);
assertOneElementOrThrowBadRequest(values, parameterName);
var value = first(values);
return parseIntOrThrowBadRequest(value, parameterName);
}
/**
* Checks whether {@code parameters} contains a parameter named {@code parameterName} that has at least one
* value that can be converted into an integer. All the values must be convertible to integer, and they are
* all converted and returned in a List.
*
* @param parameters the multivalued parameters to check
* @param parameterName name of the parameter which should be present
* @param the type of values in the multivalued map
* @return an unmodifiable List containing the int values of the validated parameter
* @throws JaxrsBadRequestException if the specified parameter is not present with at least one value or is
* not an integer
*/
public static List validateOneOrMoreIntParameters(MultivaluedMap parameters,
String parameterName) {
checkArgumentNotNull(parameters, PARAMETERS_MUST_NOT_BE_NULL);
checkArgumentNotBlank(parameterName, PARAMETER_NAME_MUST_NOT_BE_BLANK);
var values = parameters.get(parameterName);
assertOneOrMoreElementsOrThrowBadRequest(values, parameterName);
return values.stream()
.map(value -> parseIntOrThrowBadRequest(value, parameterName))
.toList();
}
@VisibleForTesting
static int parseIntOrThrowBadRequest(Object value, String parameterName) {
var result = Optional.ofNullable(value).map(Object::toString).map(Ints::tryParse);
return result.orElseThrow(() -> newJaxrsBadRequestException("'{}' is not an integer", value, parameterName));
}
/**
* Checks whether {@code parameters} contains parameter named {@code parameterName} that is a long or
* something that can be converted into a {@code long}.
*
* @param parameters the parameters to check
* @param parameterName name of the parameter which should be present
* @param the type of values in the map
* @return the long value of the validated parameter
* @throws JaxrsBadRequestException if the specified parameter is not present or is not a {@code long}
*/
public static long validateLongParameter(Map parameters, String parameterName) {
checkArgumentNotNull(parameters, PARAMETERS_MUST_NOT_BE_NULL);
checkArgumentNotBlank(parameterName, PARAMETER_NAME_MUST_NOT_BE_BLANK);
var value = parameters.get(parameterName);
assertNotNull(parameterName, value);
return parseLongOrThrowBadRequest(value, parameterName);
}
/**
* Checks whether {@code parameters} contains a parameter named {@code parameterName} that has at least one
* value that can be converted into a {@code long}. If there is more than one value, then the first one returned
* by {@link MultivaluedMap#getFirst(Object)} is returned.
*
* @param parameters the multivalued parameters to check
* @param parameterName name of the parameter which should be present
* @param the type of values in the multivalued map
* @return the long value of the validated parameter
* @throws JaxrsBadRequestException if the specified parameter is not present with at least one value or is
* not a {@code long}
*/
public static long validateOneLongParameter(MultivaluedMap parameters,
String parameterName) {
checkArgumentNotNull(parameters, PARAMETERS_MUST_NOT_BE_NULL);
checkArgumentNotBlank(parameterName, PARAMETER_NAME_MUST_NOT_BE_BLANK);
var value = parameters.getFirst(parameterName);
assertNotNull(parameterName, value);
return parseLongOrThrowBadRequest(value, parameterName);
}
/**
* Checks whether {@code parameters} contains a parameter named {@code parameterName} that has exactly one
* value that can be converted into a {@code long}. If there is more than one value, this is considered a bad request
* and a {@link JaxrsBadRequestException} is thrown.
*
* @param parameters the multivalued parameters to check
* @param parameterName name of the parameter which should be present
* @param the type of values in the multivalued map
* @return the long value of the validated parameter
* @throws JaxrsBadRequestException if the specified parameter is not present with at least one value or is
* not a {@code long}
*/
public static long validateExactlyOneLongParameter(MultivaluedMap parameters,
String parameterName) {
checkArgumentNotNull(parameters, PARAMETERS_MUST_NOT_BE_NULL);
checkArgumentNotBlank(parameterName, PARAMETER_NAME_MUST_NOT_BE_BLANK);
var values = parameters.get(parameterName);
assertOneElementOrThrowBadRequest(values, parameterName);
var value = first(values);
return parseLongOrThrowBadRequest(value, parameterName);
}
/**
* Checks whether {@code parameters} contains a parameter named {@code parameterName} that has at least one
* value that can be converted into a {@code long}. All the values must be convertible to long, and they are
* all converted and returned in a List.
*
* @param parameters the multivalued parameters to check
* @param parameterName name of the parameter which should be present
* @param the type of values in the multivalued map
* @return an unmodifiable List containing the long values of the validated parameter
* @throws JaxrsBadRequestException if the specified parameter is not present with only one value or is
* not a {@code long}
*/
public static List validateOneOrMoreLongParameters(MultivaluedMap parameters,
String parameterName) {
checkArgumentNotNull(parameters, PARAMETERS_MUST_NOT_BE_NULL);
checkArgumentNotBlank(parameterName, PARAMETER_NAME_MUST_NOT_BE_BLANK);
var values = parameters.get(parameterName);
assertOneOrMoreElementsOrThrowBadRequest(values, parameterName);
return values.stream()
.map(value -> parseLongOrThrowBadRequest(value, parameterName))
.toList();
}
@VisibleForTesting
static long parseLongOrThrowBadRequest(Object value, String parameterName) {
var result = Optional.ofNullable(value).map(Object::toString).map(Longs::tryParse);
return result.orElseThrow(() -> newJaxrsBadRequestException("'{}' is not a long", value, parameterName));
}
@VisibleForTesting
static void assertOneElementOrThrowBadRequest(List values, String parameterName) {
String message = null;
if (isNullOrEmpty(values)) {
message = parameterName + " has no values, but exactly one was expected";
} else if (values.size() > 1) {
message = parameterName + " has " + values.size() + " values, but only one was expected";
}
if (nonNull(message)) {
throw new JaxrsBadRequestException(message, parameterName);
}
}
@VisibleForTesting
static void assertOneOrMoreElementsOrThrowBadRequest(List values, String parameterName) {
if (isNullOrEmpty(values)) {
var message = parameterName + " has no values, but expected at least one";
throw new JaxrsBadRequestException(message, parameterName);
}
}
@VisibleForTesting
static JaxrsBadRequestException newJaxrsBadRequestException(String messageTemplate,
Object value,
String parameterName) {
var message = f(messageTemplate, value);
return new JaxrsBadRequestException(message, parameterName);
}
}