Only add form field if it is not null in OkHttp - java

I've been to asked to refactor some code that makes a request to a web API that I have no idea what happens when it receives the request. I just have to clean up the code that makes the request. I now have this:
FormBody formBody = new FormBody.Builder()
.add("task", task.get("task"))
.add("status", task.get("status"))
.add("spent_time", task.get("spentTime"))
.add("impediments", task.get("impediments"))
.add("reoccurring", String.valueOf(task.get("reoccurring")))
.build();
return new OkHttpClient().newCall(
new Request.Builder()
.url(buildUrl("/activities/" + task.get("id")))
.method("POST", formBody)
.header("Accept", "application/json")
.build()
Originally, this function was split into three more functions.
One url call for having the forms task and status
another url call for having spent_time and impediments
and then another url call for only having reoccurring
But since they all belong to the same url, I've decided to combine them into one function because I had an idea. Though it seems my idea kinda falls short. How do I make it so that if (e.g.) task,status, and reoccurring are null, then it would only create a form that only has spent_time and impediments?

It looks like your task variable is a map of some sorts that has String keys and Object values. Here is your solution:
Builder builder = new FormBody.Builder();
String[] names = { "task", "status", "reoccurring", "spent_time", "impediments" };
for (String name : names) {
Object value = task.get(name);
if (value != null)
builder.add(name, value instanceof String ? (String) value : String.valueOf(value));
}
FormBody formBody = builder.build();
This iterates through all your form keys and checks whether the values are null or not before adding them to your form builder.
If you want you can change this to use Streams, which might be more efficient and shorter.

Related

How to send Entity in API DELETE with HTTP-CLIENT

I have to call a DELETE Method inside one of the APIS of a client.
This shouldn´t be a problem but I am struggling with the framework that my company currently uses and I hope I can get some info that hopefully will help me solve the problem:
First thing first.
1: The api doesn´t work if I do the call sending the params as URL:
2: It works completely OK if I send the params inside the body as x-www-form-urlencoded but not form-data or raw
The documentation of the method inside the API told us the following: (Important to Look to IDEtapa)
I have to do this call in JAVA (JAVA 8)
currently my company uses HTTP_CLIENT as the main framework for APICalls.
My code:
The build of the Data (Currently I build both, as Entity and as Params for you to view I´ve tried with each one of them indepently):
Map datosApi = new HashMap<>();
datosApi.put(Constants.URL, anular_cita);
Map headers = new HashMap<>();
headers.put(Constants.AUTHORIZATION, params.get("token_autorizacion"));
headers.put("Content-Type", "application/json");
headers.put("entity_charset", "UTF-8");
datosApi.put(Constants.HEADERS, headers);
JSONObject entity = new JSONObject();
Map param = new HashMap();
param.put(Constants.ID_CENTRO, consul);
param.put("IdAsistencia", propiedades[0]);
param.put("IdCapitulo", propiedades[1]);
param.put("IdEtapa", Integer.valueOf(propiedades[2]));
entity.put(Constants.ID_CENTRO, consul);
entity.put("IdAsistencia", propiedades[0]);
entity.put("IdCapitulo", propiedades[1]);
entity.put("IdEtapa", Integer.valueOf(propiedades[2]));
datosApi.put("entity", entity.toString());
datosApi.put("entity_mime_type", "application/json");
datosApi.put("entity_charset", "UTF-8");
datosApi.put("params", param);
String anularCita = APIDao.callAPIDelete(datosApi);
The preparation for my call to the framework:
public static String callAPIDelete(Map in) {
String contentString = "";
Map res = new HashMap<>();
try {
res = XWM.execute("delete#http_client", in);
byte[] content = (byte[]) res.get("content");
contentString = new String(content, StandardCharsets.UTF_8);
And inside our framework we have this:
if (StringUtils.equals(StringUtils.trim(method), "delete"))
{
StringBuilder requestUrl = new StringBuilder(url);
if (formparams != null)
{
if (requestUrl.indexOf("?")==-1) requestUrl.append("?");
else requestUrl.append("&");
requestUrl.append(URLEncodedUtils.format(formparams, charset));
}
if (entityRequest != null)
{
log.error("Param 'entity' no puede usarse en get (se ignora)");
}
HttpDelete delete = new HttpDelete(requestUrl.toString());
delete.setConfig(requestConfig);
uriRequest = delete;
}
}
}
}
// Headers
if (headers != null)
{
for (String h: headers.keySet()) uriRequest.addHeader(h, headers.get(h));
}
// Ejecutamos método
log.info("Executing request " + uriRequest.getRequestLine());
CloseableHttpResponse response = null;
if (!preemtiveAuth || credsProvider == null)
{
response = httpclient.execute(uriRequest);
}
As you can see, in the delete method, we ignore the entity that i´ve built in the first patch of code.
The HTTPDelete Class is APACHE, the url with info of the class is the following:
https://www.javadoc.io/doc/org.apache.httpcomponents/httpclient/4.5.2/org/apache/http/client/methods/HttpDelete.html
The question can be divided in two:
1: Can we send the Entity in a Delete Call? I have found a few info about this in the following places:
Is an entity body allowed for an HTTP DELETE request?
https://web.archive.org/web/20090213142728/http://msdn.microsoft.com:80/en-us/library/cc716657.aspx
https://peterdaugaardrasmussen.com/2020/11/14/rest-should-you-use-a-body-for-your-http-delete-requests/
I assume that in order to do it, i would need a new HttpDelete that would allow us to use entity, if possible, could you give me some examples of this?
2: For what i understand of the links i posted above, while using entity in Delete calls is not forbidden is something that is not encouraged, Should I just talk with the people who made the API and ask them to change their configuration to allow us to send the info by params? (It is not personal or sensible info, just a bunch of IDs)
Thank you so much for your attention and my apologies for any typo or format mistake, still learning how to make good posts.
EDIT: i have found this answer setEntity in HttpDelete that more or less solve the first issue about if its, posible to send an Entity in a Delete call, but still, don´t now if it should be better to ask them to change their method
As told in the coments by Ewramner and VoiceOfUnreason and in the edits:
1: The answer about how to make this was found in an older post of StackOverflow: setEntity in HttpDelete
2: The answer about "Should I just talk with the people who made the API and ask them to change their configuration to allow us to send the info by params?" was answered by both of them. While it´s not forbidden, it´s something thats not recommended.
My course of action will be:
1: Talk with the people responsible for the API to give them info about this situation.
2: Ask our Architecture team to create a new method that will allow us to do HttpDelete calls with an entity body, just in case that we have to make more API calls like this one.
With this i think all my answers are solved.
Again, Thank you.

How to dynamically remove empty query parameters?

I am sending some get query to an API that returns a 400 Bad request when I send empty query parameters.
However, I do not know in advance which optional query parameters will be empty.
Apart from testing each optional parameter for emptyness, is there a simpler approach to dynamically remove empty parameters ?
Currently I am doing:
private URI getApiUrl(QueryParametersObject qpo) {
String url = configBundle.getPropertyStr("some.api.url");
UriComponentsBuilder builder = UriComponentsBuilder
.fromHttpUrl(url)
.queryParam("mandatoryParam1", qpo.getMandatoryParam1()) // MANDATORY
.queryParam("mandatoryParam2", qpo.getMandatoryParam2()); // MANDATORY
if (!qpo.getOptionalParam1().isEmpty())
builder.queryParam("optionalParam1", qpo.getOptionalParam1());
if (!qpo.getOptionalParam2().isEmpty())
builder.queryParam("optionalParam2", qpo.getOptionalParam2());
if (!qpo.getOptionalParam3().isEmpty())
builder.queryParam("optionalParam3", qpo.getOptionalParam3());
return builder.build().encode().toUri();
}
It can become quite cumbersome when the number of optional query parameters grows.
A way to do this is to use a newer version of spring-web, like 5.3.4, in which the class UriComponentsBuilder gets a new method UriBuilder queryParamIfPresent(String name, Optional<?> value) that you can use like this:
private URI getApiUrl(QueryParametersObject qpo) {
String url = configBundle.getPropertyStr("some.api.url");
UriComponentsBuilder builder = UriComponentsBuilder
.fromHttpUrl(url)
.queryParam("mandatoryParam1", qpo.getMandatoryParam1()) // MANDATORY
.queryParam("mandatoryParam2", qpo.getMandatoryParam2()) // MANDATORY
.queryParamIfPresent("optionalParam1", Optional.of(qpo.getOptionalParam1()))
.queryParamIfPresent("optionalParam2", Optional.of(qpo.getOptionalParam2()))
.queryParamIfPresent("optionalParam3", Optional.of(qpo.getOptionalParam3()));
return builder.build().encode().toUri();
}
Further enhancing the accepted answer by #l0r3nz4cc10
Optional.ofNullable would be a better approach !!!
private URI getApiUrl(QueryParametersObject qpo) {
String url = configBundle.getPropertyStr("some.api.url");
UriComponentsBuilder builder = UriComponentsBuilder
.fromHttpUrl(url)
.queryParam("mandatoryParam1", qpo.getMandatoryParam1()) // MANDATORY
.queryParam("mandatoryParam2", qpo.getMandatoryParam2()) // MANDATORY
.queryParamIfPresent("optionalParam1", Optional.ofNullable(qpo.getOptionalParam1()))
.queryParamIfPresent("optionalParam2", Optional.ofNullable(qpo.getOptionalParam2()))
.queryParamIfPresent("optionalParam3", Optional.ofNullable(qpo.getOptionalParam3()));
return builder.build().encode().toUri();

Create a reusable method to build POST call to accept more than 1 headers

I have a post method to accept 4 parameters.
RequestBody body = RequestBody.create(type, postData);
Request request = new Request.Builder()
.url(url)
.addHeader("Authorization", ssid)
.post(body)
.build();
But few of my calls have more than one header. I can take care of passing the values through a context map. But, how can I build the request in a manner to accept multiple header values.
Example, the above code accepts authorization header.
Another call has two more headers tag1, tag2. the method should be able to tell that 2 sets of values have been passed to it and should build the request accordingly. Is it posssible?
You don't have to build the result immediately. You can store the result of Request.Builder() in a variable, loop over your headers, and add them to the builder. This is one of the positives of using a builder instead of a constructor because up until the point that you call build(), you can keep modifying what will be built using whatever loops or function you would like to use to modify the builder.
public static Request requestWithHeaders(String url, RequestBody body, List<String> headerNames, List<String> headerValues) {
if (headerNames.size() != headerValues.size()) {
throw new IllegalArgumentException(String.format(
"Must provide the same number of header names and header values. "
+ "%d header names and %d header values provided",
headerNames.size(),
headerValues.size()));
}
Request.Builder builder = new Request.Builder()
.url(url)
.post(body);
for (int i = 0; i < headerNames.size(); ++i) {
builder.addHeader(headerNames.get(i), headerValues.get(i));
}
return builder.build();
}
You can then call it like this for instance:
Request request = requestWithManyHeaders(
url,
body,
Arrays.asList("auth", "tag1", "tag2"),
Arrays.asList("authValue", "tag1Value", "tag2Value")
);

Changing the format of list / array in URL [Retrofit 2]

As stated in the Retrofit documentation above the #Query annotation:
Passing a List or array will result in a query parameter for each
non-null item.
As of now my call looks something like this:
#GET("questions")
Call<List<QuestionHolder>> getQuestionsExcludingTheSpecified(
#Query("exclude_ids") long[] excludedQuestionIds
);
This works but results in fairly long URLs quite fast.
E.g. for excludedQuestionIds = new long[]{1L, 4L, 16L, 64L} the request URL already will be /questions?exclude_ids=1&exclude_ids=4&exclude_ids=16&exclude_ids=64.
Is there an easy way to exchange this behaviour resulting in arrays formatted as exclude_ids=[1,4,16,64] or something similar?
What came to my mind yet was, to:
use JsonArray as parameter, but then I need to convert every array / list before making the call
intercept every request and compress duplicated keys
override the built-in #Query decorator
Any ideas?
I decided to go with the Interceptor approach. I simply change any outgoing request that includes more than one value for a single query parameter.
public class QueryParameterCompressionInterceptor implements Interceptor {
#Override
public Response intercept(Interceptor.Chain chain) throws IOException {
Request request = chain.request();
HttpUrl url = request.url();
for (String parameterName : url.queryParameterNames()) {
List<String> queryParameterValues = url.queryParameterValues(parameterName);
if (queryParameterValues.size() > 1) {
String formattedValues= "[" + TextUtils.join(",", queryParameterValues) + "]";
request = request.newBuilder()
.url(
url.newBuilder()
.removeAllQueryParameters(parameterName)
.addQueryParameter(parameterName, formattedValues)
.build()
).build();
}
}
return chain.proceed(request);
}
non android solution
TextUtils is part of the Android SDK, in case you're not developing for Android you might exchange TextUtils.join for a method like this:
public static String concatListOfStrings(String separator, Iterable<String> strings) {
StringBuilder sb = new StringBuilder();
for (String str : strings) {
sb.append(separator).append(str);
}
sb.delete(0, separator.length());
return sb.toString();
}
}
You may also have a look at this SO question for more solutions regarding the concatenation.

I do not understand why I can not add object.getter() to MultivaluedMap

I tried to send POST request with some parameters. For this I form MultivaluedMap
if I make this adding to MultivaluedMap
String ban = subscriber.getBan();
String username = user.getUsername();
postData.add("agent", username);
postData.add("datasource", "online");
postData.add("accountId", ban);
String json = RESTUtil.doPost(url, postData);
All work fine
but if I make this
postData.add("agent", user.getUsername());
postData.add("datasource", "online");
postData.add("accountId", subscriber.getBan());
String json = RESTUtil.doPost(url, postData);
I have error:
com.sun.jersey.api.client.ClientHandlerException: java.lang.ClassCastException: java.lang.String cannot be cast to java.util.List
It is my post method
public static String doPost(String url, MultivaluedMap formData) {
try {
Client client = Client.create();
WebResource wr = client.resource(url);
client.setConnectTimeout(CONNECTION_TIMEOUT);
client.setReadTimeout(READ_TIMEOUT);
ClientResponse response2 = wr
.accept("application/json;")
.type("application/x-www-form-urlencoded; charset=UTF-8")
.post(ClientResponse.class, formData);
if (response2.getStatus() != 200) {
throw new RuntimeException("Failed : HTTP error code : " + response2.getStatus());
}
return response2.getEntity(String.class);
} catch (Exception e) {
LOG.log(Level.WARNING, "callRestUrl:", e);
JsonObject jo = new JsonObject();
jo.addProperty("resultCode", "EXCEPTION");
jo.addProperty("details", e.getMessage());
return GSON.toJson(jo);
}
}
And in second case I get error after .post(ClientResponse.class, formData);
I do not understand what is wrong. subscriber.getBan() and user.getUsername() return String like ban and username, but if I use the getter, a have error.
And part 2. I found this article this article
but I do not understand when to use add or put and their difference?
Can you explicit how you create your instance of MultivaluedMap? How are you using it?
MultivaluedMap is a couple of key (single value) and value (list of objects). See the declaration:
public interface MultivaluedMap<K,V> extends Map<K,List<V>>
I suppose both of your snippets are used sequentially in the same piece of code. I the first case, you initialise the value for the key 'ban' (that's mean: the value is a list of one element), in the second case, you add a value to the list to the same key 'ban'. It happens exactly the same for the key 'username'.
I your first case, Java automatically catch the list (of one value) to a string, after the second add, this cannot be the case.
To verify it, you can simply change the order (do first your second case, then the first one). You should get the same error, after the second.
To resolve your case, consider using the method putSingle instead of add if you want to "update/replace" the value, or re-initiate your instance of formData (formData = new ...) before using it another time.

Categories