org.kiwiproject.jaxrs.client.WebTargetHelper Maven / Gradle / Ivy
package org.kiwiproject.jaxrs.client;
import static java.util.Objects.isNull;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.kiwiproject.base.KiwiPreconditions.checkArgumentNotBlank;
import static org.kiwiproject.base.KiwiPreconditions.checkArgumentNotNull;
import static org.kiwiproject.base.KiwiPreconditions.requireNotNull;
import static org.kiwiproject.collect.KiwiArrays.isNullOrEmpty;
import static org.kiwiproject.collect.KiwiLists.isNullOrEmpty;
import static org.kiwiproject.collect.KiwiMaps.isNullOrEmpty;
import com.google.common.annotations.Beta;
import com.google.common.annotations.VisibleForTesting;
import lombok.experimental.Delegate;
import org.apache.commons.lang3.StringUtils;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.WebTarget;
import javax.ws.rs.core.MultivaluedMap;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Stream;
/**
* Use with JAX-RS {@link WebTarget} instances to provide convenient functionality when adding query parameters.
* Most of this functionality is intended for cases when you only want to add parameters when they are not null (or not
* blank in the case of Strings). If you want a query parameter to be added regardless of whether a value is present
* or not, use the regular {@link WebTarget#queryParam(String, Object...) queryParam} method in {@code WebTarget}.
*
* The methods provided by this helper class allow you to either require query parameters or include them only when
* they have a value. When you require a query parameter, an {@link IllegalArgumentException} is thrown when
* a caller does not supply a name or value. Other methods allow you to optionally include one or more query
* parameters, as well as add them from a {@link Map} or a {@link MultivaluedMap}, such that only non-null/non-blank
* values are added.
*
* Usage example (assuming {@link WebTargetClientHelper#withClient(Client) withClient} is statically imported):
*
* withClient(client).target("/search")
* .queryParamRequireNotBlank("q", query)
* .queryParamIfNotBlank("sort", sort)
* .queryParamIfNotBlank("page", page)
* .queryParamIfNotBlank("limit", limit)
* .queryParamFilterNotBlank("langs", langs);
*
* Limitations
* This is a limited wrapper around {@link WebTarget} that provides enhanced functionality only for
* adding query parameters. Only the methods defined in this class are chainable, i.e. once you call a method defined
* in the regular {@link Client} interface, you leave the {@link WebTargetHelper} context.
*
* For example you can NOT do this:
*
* withClient(client).target("/search")
* .queryParamRequireNotBlank("q", query)
* .queryParam("sort", sort) // after this, only Client methods are accessible!!! WON'T COMPILE
* .queryParamIfNotBlank("page", page)
* .queryParamIfNotBlank("limit", limit)
* .queryParamFilterNotBlank("langs", langs);
*
* With the current basic implementation, this means certain usages will be awkward. For example, when using
* both parameter templates and query parameters, the query parameters need to be added first, for the reason
* given above about leaving the {@link WebTargetHelper} context. For example:
*
* var response = withClient(client).target("/users/{userId}/trades/{tradeId}")
* .queryParamIfNotBlank("displayCurrency", currency)
* .queryParamIfNotNull("showLimitPrice", showLimitPrice)
* .resolveTemplate("userId", userId) // after this, only Client methods are accessible!!!
* .resolveTemplate("tradeId", tradeId)
* .request()
* .get();
*
* One way to get around this restriction is to use methods from {@link WebTarget} as normal, and then wrap it
* with a {@link WebTargetHelper} to add query parameters. The above example would then look like:
*
* var pathResolvedTarget = client.target("/users/{userId}/trades/{tradeId}")
* .resolveTemplate("userId", userId)
* .resolveTemplate("tradeId", tradeId);
*
* var response = withWebTarget(pathResolvedTarget)
* .queryParamIfNotBlank("displayCurrency", currency)
* .queryParamIfNotNull("showLimitPrice", showLimitPrice)
* .request()
* .get();
*
* This usage allows for full functionality of {@link WebTarget} while still getting the enhanced query parameter
* features of this class. It isn't perfect, but it works and, in our opinion anyway, doesn't intrude too much on
* building JAX-RS requests. In other words, we think it is a decent trade off.
*
* @implNote Internally this uses Lombok's {@link Delegate}, which is why this class doesn't implement {@link WebTarget}
* directly. While this lets us easily delegate method calls to a {@link WebTarget}, it also restricts what we can do
* here, and is the primary reason why there are usage restrictions. However, in our general usage this implementation
* has been enough for our needs. Nevertheless, this is currently marked with the Guava {@link Beta} annotation in case
* we change our minds on the implementation.
*/
@Beta
public class WebTargetHelper {
@Delegate
private final WebTarget webTarget;
/**
* Package-private constructor. Used by {@link WebTargetClientHelper}.
*
* @param webTarget the WebTarget to wrap
*/
WebTargetHelper(WebTarget webTarget) {
this.webTarget = requireNotNull(webTarget, "webTarget must not be null");
}
/**
* @return the wrapped WebTarget
*/
@VisibleForTesting
WebTarget wrapped() {
return webTarget;
}
/**
* Convert the current state contained in this helper to a new {@link WebTarget} instance.
*
* @return a new WebTarget instance
*/
public WebTarget toWebTarget() {
return webTarget.path("");
}
/**
* Create a new instance with the given {@link WebTarget}.
*
* @param webTarget the WebTarget to use
* @return a new instance
*/
public static WebTargetHelper withWebTarget(WebTarget webTarget) {
return new WebTargetHelper(webTarget);
}
/**
* Add the required query parameter.
*
* @param name the parameter name
* @param value the parameter value
* @return this instance
* @throws IllegalArgumentException if name is blank or value is null
*/
public WebTargetHelper queryParamRequireNotNull(String name, Object value) {
checkArgumentNotBlank(name, "name cannot be blank");
checkArgumentNotNull(value, "value cannot be null for parameter %s", name);
var newWebTarget = webTarget.queryParam(name, value);
return new WebTargetHelper(newWebTarget);
}
/**
* Add the given query parameter only if name is not blank and value is not null.
*
* @param name the parameter name
* @param value the parameter value
* @return this instance
*/
public WebTargetHelper queryParamIfNotNull(String name, Object value) {
if (isBlank(name) || isNull(value)) {
return this;
}
var newWebTarget = this.webTarget.queryParam(name, value);
return new WebTargetHelper(newWebTarget);
}
/**
* Adds any non-null values to the given query parameter. If name is blank, this is a no-op.
*
* @param name the parameter name
* @param values one or more parameter values
* @return this instance
*/
public WebTargetHelper queryParamFilterNotNull(String name, Object... values) {
if (isBlank(name) || isNullOrEmpty(values)) {
return this;
}
return queryParamFilterNotNull(name, Arrays.stream(values));
}
/**
* Adds any non-null values to the given query parameter. If name is blank, this is a no-op.
*
* @param name the parameter name
* @param values one or more parameter values
* @return this instance
*/
public WebTargetHelper queryParamFilterNotNull(String name, List