The user can set a number of values (which are optional) for a search query that will be passed on to a REST api.
So I create a POJO to hold all the set values like
class Search{
private String minPrice;
private String maxPrice;
private String category;
// etc.pp.
}
When I construct the URL for the api call, i have to check wether that value is set all the time like
if (search.getMinPrice() != null){
url.append("&minprice=" + search.getMinPrice());
}
Is there a more convenient/ elegant way to do this
with pure Java, just a way to "do sth if sth is not null"
or even a library/tool that lets you construct Urls from objects?
Something like:
String url = "http://base.com/some/path?" + (maxPrice==null ? "" : "maxPrice="+maxPrice);
Related
I am new to Java and Vertx and I have a query string with the following format:
GET /examples/1/data?date_1[gt]=2021-09-28&date_1[lt]=2021-10-28
Here I have this date_1 parameter which is within a certain range. I have been using HttpServerRequest class to extract simple parameters like integers but not sure how to proceed with these kind of range parameters.
With the simple parameters, I can do something like:
String param = request.getParam(paramName);
paramAsInteger = Integer.valueOf(paramAsString);
However, confused as to how to deal with the gt and lt options and the fact that we have same parameter twice.
You say that you have difficulties parsing out these tokens. Here's how you can handle this.
The first thing to understand is that the parameter name is NOT "date1"
There are actually two parameters here
2.1. "date_1[gt]" with a value of "2021-09-28"
2.2. "date_1[lt]" with a value of "2021-10-28"
This is because in the URI parameter definition everything before the "=" sign is the parameter name and everything after is the parameter value.
You can just do
String dateAsString = request.getParam("date1[gt]");
paramAsInteger = toDate(dateAsString)
To implement the toDate() function read this simple article how to convert a string object into a data object using a standard library
(link)
Vert.x will treat these parameters as two separate ones. So RoutingContext#queryParam("date_1[gt]") will only give you the value for [gt]. If you want the value for [lt] you need to get that separately.
That being said, you can move this tedious logic into an extra handler and store the values in the RoutingContext. Something like this might be easier:
private void extractDates(RoutingContext ctx) {
var startDate = ctx.queryParam("date_1[gt]");
var endDate = ctx.queryParam("date_1[lt]");
var parsedStartDate = DateTimeFormatter.ISO_LOCAL_DATE.parse(startDate.get(0));
var parsedEndDate = DateTimeFormatter.ISO_LOCAL_DATE.parse(endDate.get(0));
// things we put in the context here can be retrieved by later handlers
ctx.put("startDate", parsedStartDate);
ctx.put("endDate", parsedEndDate);
ctx.next();
}
Then, in your actual handler you can access the two dates as follows:
router.get("/date")
.handler(this::extractDates)
.handler(ctx -> {
var responseBody = ctx.get("startDate") + " - " + ctx.get("endDate");
ctx.end(responseBody);
});
This allows you to keep your actual business logic concise.
For the sake of simplicity lets assume I have a Document object with seven fields (but imagine that it can have many more). This object looks something like this:
#Getter
#Setter
public class Document {
private String fileName;
private String fileType;
private String createdBy;
private Date createdAt;
private Date lastModifiedAt;
private List<String> modifiers;
private Long timesModified;
}
I want to create an endpoint which can receive any number of #RequestParam and returns a List<Document> of all the documents which match the given query. For example: return all documents with fileType == doc, which were created between createdAt == 01/01/2021 && createdAt 31/01/2021, modified timesModified == 5 times and modifiers.contains("Alex"). The reason for this is that I want to allow the user to query for documents depending on combination of fields the user wants. Originally to handle this we created the endpoint like so:
#GetMapping(value = {RestApi.LIST})
public ResponseEntity<List<Document>> getDocuments (#RequestParam Map<String, Object> optionalFilters) {
List<Document> documents = documentService.getListOfDocuments(optionalFilters);
if (documents != null) {
return new ResponseEntity<>(documents, HttpStatus.OK);
}
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
The problem with this is that because we use optionalFilters as Map<String, Object> this requires us to perform a lot of casting in our code and overall makes our code very tedious and cumbersome because we have to iterate through the whole map and create a custom query depending the fields that were passed. In order to try and improve this I created an OptionalFilters object:
#Getter
#Setter
#NoArgsConstructor
public class OptionalFilters {
private String fileName;
private String fileType;
private String createdBy;
private Date createdAt;
private Date lastModifiedAt;
private List<String> modifiers;
private Long timesModified;
}
And modified the endpoint to this:
#GetMapping(value = {RestApi.LIST})
public ResponseEntity<List<Document>> getDocuments (#Valid OptionalFilters optionalFilters) {
List<Document> documents = documentService.getListOfDocuments(optionalFilters);
if (documents != null) {
return new ResponseEntity<>(documents, HttpStatus.OK);
}
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
However, although that this simplifies the way we receive the parameters and extract the values from them, we still need to iterate through all the parameters and create a custom query. Is there some way to elevate and take advantage of Spring-Data (or any other solution) so that I don't have to create a custom query depending on each query param that is passed through? I am using Solr as the repository if this may be any help.
Using Query by Example is one the most simple option but it has its limitations. Excerpt from the above link:
Limitations
Like all things, the Query by Example API has some limitations. For instance:
Nesting and grouping statements are not supported, for example:
(firstName = ?0 and lastName = ?1) or seatNumber = ?2
String matching only includes exact, case-insensitive, starts, ends, contains, and regex
All types other than String are exact-match only
Query by Example is suitable choice if your filtering is never too complicated. But when restirictions like above hit the fan of your CPU cooler the choice is to use Specifications to construct queries.
One big difference is also that Using Query by Example you need to explicitly populate the example by its getters and setters. With specification you can make it in a generic way (with Java generics) using just use field names
In your case you could just pass the map to generic method and create filtering by just looping and adding by and (note that the link's example has static stuff mostly but it has not to be, you just need field name/criterion -pair to loop it in a generic way)
With specifications you can do anything that can be done with Query by Example and almost anything else also. The overhead to get familiar with specifications might be bigger but the advantage using specifications will be rewarding.
In a nutshell:
Spring interface Specification is based on JPA CriteriaQuery and for each you need only to implement one method:
Predicate toPredicate (Root<User> root, CriteriaQuery<?> query, CriteriaBuilder builder);
Repository interfaces needs just to extend JpaSpecificationExecutor<YourClass> When you have a set of predicates, you can - for example -
repository.findAll(Specification.where(spec1).and(spec2));
It might seem complicated or difficult at start but it is not that at all. The greatest advantage with Specification is that you can do almost anything programmatically instead of manipulating JPQL queries or so.
I have query string like that:
ObjectGUId=1abcde&ObjectType=2&ObjectTitle=maximumoflife&Content=racroi&TimeStamp=2012-11-05T17:20:06.056
And I have Java Object:
LogObject{
private String ObjectGUId;
private String ObjectType;
private String ObjectTitle;
private String Content;
private String TimeStamp;
}
So i want to parse this query string to this java Object.
I've searched and read many question but not gotten correct answer yet.
Show me what can solve this problem.
Inspired by #bruno.braga, here's a way using Apache http-components. You leverage all the parsing corner cases:
List<NameValuePair> params =
URLEncodedUtils.parse("http://example.com/?" + queryString, Charset.forName("UTF-8"));
That'll give you a List of NameValuePair objects that should be easy to work with.
If you do not really need to push the querystring into your own class (you might want that though), instead of parsing it manually, you could use the URLDecoder, as #Sonrobby has commented:
String qString = "ObjectGUId=1abcde&ObjectType=2&ObjectTitle=maximumoflife";
Uri uri = Uri.parse(URLDecoder.decode("http://dummy/?" + qString, "UTF-8"));
if (uri != null) {
for(String key: uri.getQueryParameterNames()) {
System.out.println("key=[" + key + "], value=[" + uri.getQueryParameter(key) + "]");
}
}
The "dummy" looks dirty but it is required if what you only have is the querystring values (qString). If you have the complete URL, just pass it directly to the URLDecoder, and you are done.
Etiquette
You really should be much more specific about what you have tried and why it didn't work.
A proper code sample of your LogObject would really be very helpful here.
Ideally, you would provide a SSCCE so others could easily test your problem themselves.
Answer
You can extract the name:value pairs like this:
String toParse = "ObjectGUId=1abcde&ObjectType=2&ObjectTitle=maximumoflife&Content=racroi&TimeStamp=2012-11-05T17:20:06.056";
String[] fields = toParse.split("&");
String[] kv;
HashMap<String, String> things = new HashMap<String, String>();
for (int i = 0; i < fields.length; ++i)
{
t = fields[i].split("=");
if (2 == kv.length)
{
things.put(kv[0], kv[1]);
}
}
I have chosen to put them into a HashMap, but you could just as easily look at the name part (kv[0]) and choose to do something with it. For example:
if kv[0].equals("ObjectGUId")
{
logObject.setGUId(kv[1]); // example mutator/setter method
}
else if //...
However, all your fields in LogObject are private and you haven't shown us any methods, so I hope you have some way of setting them from outside... bear in mind you will need to store the pairs in a data structure of some kind (as I have done with a HashMap) if you intend to intialise a LogObject with all the fields rather than setting the fields after a constructor call.
Speaking of SSCCEs, I made one for this answer.
I've been experimenting with the Java Play Framework 2.0 for a few weeks now, but I am now struggling with the following:
How can I pass Java object from one Play template to another?
I can pass simple objects about no problem:
GET /Login/:email controllers.Application.login(email:String)
With the following code in my controller:
public static Result login(String email) {
//Do some stuff
return ok("");
}
But what I need to be able to is something like this:
GET /Test/:user controllers.Application.test(user:User)
With the following code in my controller:
public static Result test(User user) {
//Do some stuff
return ok("");
}
When I try compiling, I get the following error:
not found: type User
Does anybody know what I need to do to get this working? Is it even possible? Appreciate any help!
Using basic (don't want to write 'ordinary') types as routes params is clean approach which will not cause a headache. For an example instead trying to send whole object it's better to send its id - most probably some kind of numeric type or unique String for an example if your user model has id of Long type you can just do it as easy as:
GET /Test/:userId controllers.Application.test(userId: Long)
controller
public static Result test(Long userId) {
User user = User.find.byId(userId);
return ok("Now you're watching user: " + user.name);
}
Other useful sample are booleans which, instead passing it it's just easier use 0/1 Int/int types (with default value set as false):
GET /set-admin/:userId controllers.App.setAdmin(userId: Long, setTo: Int ?= 0)
or true
GET /set-admin/:userId controllers.App.setAdmin(userId: Long, setTo: Int ?= 1)
controller
public static Result setAdmin(Long userId, int setTo) {
User user = User.find.byId(userId);
user.isAdmin = (setTo == 1); // of course isAdmin field of User model is type of Boolean
user.update(id);
return ok("User " + user.name + " is " + user.isAdmin);
}
so in template you can just make a link:
<a href='#routes.App.setAdmin(user.id, 1)'>Set as admin</a>
<a href='#routes.App.setAdmin(user.id, 0)'>Set as common user</a>
LinkedIn rest API for people search has following format http://api.linkedin.com/people-search?search-param1=val1&search-param2=val2...
Since I intend to use this many time in my application, I am trying to create a Search object like this where we can set values for different search parameters and then a method called generateQueryUrl to generate the url in above format.
public class Search {
private String searchParam1;
private String searchParam2;
public void setSearchParam1(String val) { this.searchParam1 = val; }
public void setSearchParam2(String val) { this.searchParam2 = val; }
//Form the query url
public String generateQueryUrl(){
String url = "";
if(searchParam1 != null) {
url += "search-param1=" + searchParam1 + "&";
}
if(searchParam2 != null) {
url += "search-param2=" + searchParam2 + "&";
}
return url;
}
My question is, is there a better pattern/design to do this? If there are many parameters, checking for NULL and then appending corresponding parameter name, value seems to add redundant code to me.
Also please let me know if this approach is fine.
I think what you are designing is in fact a "Builder", in that you are building the URL that will be returned at the end. Think StringBuilder, or Apache's EqualsBuilder, HashCodeBuilder, etc.
For a more theoretical explanation, check http://sourcemaking.com/design_patterns/builder.
Now, as for your code, to properly build the URL, I would use "set" methods like you do, but inside them I would use Apache's HttpComponents (ex HttpClient) to properly append the parameters of the URL.
Think about it, if you insert a "&" in the value of one of the parameter, your query parameters will be messed up since you normally have to escape this character (and you don't seem to be doing it here). I prefer letting tested and known APIs (UriUtils) do that for me.
So, my class would look like so using Apache's HttpComponents:
public class SearchBuilder {
private URI baseUri;
private List<NameValuePair> parameters;
public SearchBuilder (URI baseUri) {
this.baseUri= baseUri;
this.parameters = new ArrayList<NameValuePair>();
}
public void addSearchParam1(String val) {
if(!StringUtils.isBlank(val)) {
parameters.add(new BasicNameValuePair("SearchParam1", val));
}
}
//Form the query url
public URI toURI(){
URI uri = URIUtils.createURI(baseUri.getScheme(), baseUri.getHost(), baseUri.getPort(), baseUri.getPath(), URLEncodedUtils.format(parameters, "UTF-8"), null);
return uri;
}
Edit: I added the "isBlank" check to ensure that parameters with null, empty of whitespace-only Strings will not be added to the query. Now, you can change that to check only for null, but I'm sure you got the idea.
You can maintain a Map with keys as the param names and values as the values. Your setter methods can put keys and values in the Map. Or, you can have one generic setter which accepts the name & the value.
In the generateQueryUrl method, you can just iterate over the Map.
You may also want to URLEncode the values if not being done at the later stage.