patternjavaMinor
Extracting various URL parameters in Rest Service and then passing it to Builder class
Viewed 0 times
restclassbuilderpassingserviceextractingthenandparametersvarious
Problem
I have a service which takes a lot of key value parameters in a URL. Only a few are mandatory and others are optional. It's up to the customer whether they want to set those optional parameters or not.
The mandatory parameters are
Now if the full URL is to be made along with other optional parameters, then the URL will look like this:
Here are the parameters my URL will take and the same parameters I am setting it in my
```
import javax.ws.rs.core.MultivaluedMap;
private final DataClient dataClient = DataFactory.getInstance();
pr
The mandatory parameters are
userid or flowid. They can be set together or independent of each other so below are the possible combinations:- http://localhost:8080/base/v1/lookup?userid=12345
- http://localhost:8080/base/v1/lookup?flowid=abcdefgh
- http://localhost:8080/base/v1/lookup?userid=12345&flowid=abcdefgh
Now if the full URL is to be made along with other optional parameters, then the URL will look like this:
http://localhost:8080/base/v1/lookup?userid=12345&flowid=abcdefgh&timeout=200&dataflag=false&processflag=true&linkvtou=true&linkutou=false&maxid=3&startdate=12345&enddate=54321&holdertype=id1&holdertype=id2&attr=@kite&count=1&hide=5Here are the parameters my URL will take and the same parameters I am setting it in my
RequestKey builder class. My RequestKey builder class has default values for some optional fields like all boolean variables, maxid field, and timeout.UserId: This will always be aString(can be number as well).
FlowId: This should always be aString.
Timeout: This should be along. By default this is 200 ms.
DataFlag: This should be aboolean. By default this value is true if they are not passing.
ProcessFlag: This should be aboolean. By default this value is true if they are not passing.
LinkVToU: This should be aboolean. By default this value is true if they are not passing.
LinkUToU: This should be aboolean. By default this value is false if they are not passing.
MaxId: This will be along. By default this is 5.
StartDate: This will be along.
EndDate: This will be along.
HolderType: This is aListofString,List.
```
import javax.ws.rs.core.MultivaluedMap;
private final DataClient dataClient = DataFactory.getInstance();
pr
Solution
Here are some comments that I had when reading through your code:
-
The builder logic is awkward:
surrounded by the try-catch mixes 3 differents concepts: validation, defaults and building. The code-smell here is the long try-catch: try-catch blocks should be as minimal as possible, catching the most specific exceptions. The longer they are getting, the more chance there is that things are not decoupled enough.
-
You should first validate all your input parameters: are the boolean (resp. number) values really booleans (resp. numbers)? Are the required parameters present? If one the parameter is incorrect then you can safely return a
Note that this could use other helper methods like
-
Decoupling defaults with the building process is more trickier and context-dependent: is the builder supposed to know where the value it's using are coming from? Should it know the default values? At the very least, we can create helper methods like
-
Finally, the last part of the code is also converting a
With all those comments, you could have the following main method:
which I feel is clearer and separates each part of the code.
- Avoid calling
inputParameters.remove(key)just to retrieve the value for the given key. Not all maps support theremoveoperation, so you might end up with anUnsupportedOperationException. What you really want is togetthe value for a key, so just callinputParameters.get(key).
- I'd refactor the code mapping the
Map inputParametersintoRequestKeyobject into a dedicated method: it would avoid having the code manipulating and the code transforming the request in the same place. This is generally a good practice: make your methods small, concise and to the point.
-
The builder logic is awkward:
if (!TestUtils.isEmpty(userid)) {
builder.setUserId(userid);
}surrounded by the try-catch mixes 3 differents concepts: validation, defaults and building. The code-smell here is the long try-catch: try-catch blocks should be as minimal as possible, catching the most specific exceptions. The longer they are getting, the more chance there is that things are not decoupled enough.
-
You should first validate all your input parameters: are the boolean (resp. number) values really booleans (resp. numbers)? Are the required parameters present? If one the parameter is incorrect then you can safely return a
SC_BAD_REQUEST. I noticed in your current that you are in fact not validating that one of userid or flowid was present. We could then imagine a method containing that logic:private boolean validateInputParameters(Map inputParameters) {
// return true or false whether the parameters are correct or not
// validate that either userid or flowid are set
String userid = inputParameters.get("userid");
String flowid = inputParameters.get("flowid");
if (isEmpty(userId) && isEmpty(flowid)) {
return false;
}
//validate that dataflag is a boolean
String dataflag = inputParameters.get("dataflag");
if (!"true".equals(dataflag) && !"false".equals(dataflag)) {
return false;
}
// rest of validation ...
return true; // everything was correct
}Note that this could use other helper methods like
isNumber that would determine if a String can be parsed into a number. The advantage is that if your requirements change in the future (for example, the timeout must be less than a certain number), you only need to change this method. Even for boolean parameters, there is an advantage: in the future, you might want to add support for "t" or "f".-
Decoupling defaults with the building process is more trickier and context-dependent: is the builder supposed to know where the value it's using are coming from? Should it know the default values? At the very least, we can create helper methods like
private boolean getBooleanParameterOr(String key, boolean defaultValue) that would convert a raw String into a boolean with a default value if the value is empty. Then, we can imagine a method:private RequestKey convertToRequestKey(Map inputParameters) {
boolean dataflag = getBooleanParameterOr(inputParameters.get("dataflag"), true);
// the same for the rest of the parameters
// ...
builder.dataFlag(dataflag);
// ...
return builder.build();
}-
Finally, the last part of the code is also converting a
beResponse into a responseList. This could also be refactored into a dedicated method:private List convertToResponseList(List beResponse) {
// do transformation here
}With all those comments, you could have the following main method:
public Response lookup(@Context UriInfo uriInfo) {
Optional client = TestUtils.getClientId();
if (!client.isPresent()) {
// return SC_UNAUTHORIZED here
}
Map inputParameters = TestUtils.convertMultiValueMapToRegularMap(uriInfo.getQueryParameters());
boolean valid = validateInputParameters(inputParameters);
if (!valid) {
// return SC_BAD_REQUEST here
}
RequestKey requestKey = convertToRequestKey(inputParameters);
List beResponse = dataClient.getUserDataSync(requestKey);
List responseList = convertToResponseList(beResponse);
// return SC_OK or SC_INTERNAL_SERVER_ERROR response here
}which I feel is clearer and separates each part of the code.
Code Snippets
if (!TestUtils.isEmpty(userid)) {
builder.setUserId(userid);
}private boolean validateInputParameters(Map<String, String> inputParameters) {
// return true or false whether the parameters are correct or not
// validate that either userid or flowid are set
String userid = inputParameters.get("userid");
String flowid = inputParameters.get("flowid");
if (isEmpty(userId) && isEmpty(flowid)) {
return false;
}
//validate that dataflag is a boolean
String dataflag = inputParameters.get("dataflag");
if (!"true".equals(dataflag) && !"false".equals(dataflag)) {
return false;
}
// rest of validation ...
return true; // everything was correct
}private RequestKey convertToRequestKey(Map<String, String> inputParameters) {
boolean dataflag = getBooleanParameterOr(inputParameters.get("dataflag"), true);
// the same for the rest of the parameters
// ...
builder.dataFlag(dataflag);
// ...
return builder.build();
}private List<DataResponse> convertToResponseList(List<HolderResponse> beResponse) {
// do transformation here
}public Response lookup(@Context UriInfo uriInfo) {
Optional<String> client = TestUtils.getClientId();
if (!client.isPresent()) {
// return SC_UNAUTHORIZED here
}
Map<String, String> inputParameters = TestUtils.convertMultiValueMapToRegularMap(uriInfo.getQueryParameters());
boolean valid = validateInputParameters(inputParameters);
if (!valid) {
// return SC_BAD_REQUEST here
}
RequestKey requestKey = convertToRequestKey(inputParameters);
List<HolderResponse> beResponse = dataClient.getUserDataSync(requestKey);
List<DataResponse> responseList = convertToResponseList(beResponse);
// return SC_OK or SC_INTERNAL_SERVER_ERROR response here
}Context
StackExchange Code Review Q#117706, answer score: 4
Revisions (0)
No revisions yet.