We are now aware of what our user is interested in. It would be a good idea to improve our Tweet controller so that it allows searching from a list of keywords.
One interesting way to pass key-value pairs in a URL is to use a matrix variable. It is pretty similar to request parameters. Consider the following code:
someUrl/param?var1=value1&var2=value2
Instead of the preceding parameter, matrix variables understand this:
someUrl/param;var1=value1;var2=value2
They also allow each parameter to be a list:
someUrl/param;var1=value1,value2;var2=value3,value4
A matrix variable can be mapped to different object types inside a controller:
Map<String, List<?>>
: This handles multiple variables and multiple valuesMap<String, ?>
: This handles a case in which each variable has only one valueList<?>
: This is used if we are interested in a single variable whose name can be configuredIn our case, we want to handle something like this:
http://localhost:8080/search/popular;keywords=scala,java
The first parameter, popular
, is the result type known by the Twitter search API. It can take the following values: mixed
, recent
, or popular
.
The rest of our URL is a list of keywords. We will therefore map them to a simple List<String>
object.
By default, Spring MVC removes every character following a semicolon in a URL. The first thing we need to do to enable matrix variables in our application is to turn off this behavior.
Let's add the following code to our WebConfiguration
class:
@Override public void configurePathMatch(PathMatchConfigurer configurer) { UrlPathHelper urlPathHelper = new UrlPathHelper(); urlPathHelper.setRemoveSemicolonContent(false); configurer.setUrlPathHelper(urlPathHelper); }
Let's create a new controller in the search
package, which we will call SearchController
. Its role is to handle the following request:
package masterSpringMvc.search; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.social.twitter.api.Tweet; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.MatrixVariable; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.servlet.ModelAndView; import java.util.List; @Controller public class SearchController { private SearchService searchService; @Autowired public SearchController(SearchService searchService) { this.searchService = searchService; } @RequestMapping("/search/{searchType}") public ModelAndView search(@PathVariable String searchType, @MatrixVariable List<String> keywords) { List<Tweet> tweets = searchService.search(searchType, keywords); ModelAndView modelAndView = new ModelAndView("resultPage"); modelAndView.addObject("tweets", tweets); modelAndView.addObject("search", String.join(",", keywords)); return modelAndView; } }
As you can see, we are able reuse the existing result page to display the tweets. We also want to delegate the search to another class called SearchService
. We will create this service in the same package as SearchController
:
package masterSpringMvc.search; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.social.twitter.api.Tweet; import org.springframework.social.twitter.api.Twitter; import org.springframework.stereotype.Service; import java.util.List; @Service public class SearchService { private Twitter twitter; @Autowired public SearchService(Twitter twitter) { this.twitter = twitter; } public List<Tweet> search(String searchType, List<String> keywords) { return null; } }
Now, we need to implement the search()
method.
The search operation accessible on twitter.searchOperations().search(params)
takes searchParameters
as an argument for an advanced search. This object allows us to conduct a search on a dozen of criteria. We are interested in the query
, resultType
, and count
attributes.
First, we need to create a ResultType
constructor with the searchType
path variable. The ResultType
is an enum, so we can iterate over its different values and find one that matches the input, ignoring the case:
private SearchParameters.ResultType getResultType(String searchType) { for (SearchParameters.ResultType knownType : SearchParameters.ResultType.values()) { if (knownType.name().equalsIgnoreCase(searchType)) { return knownType; } } return SearchParameters.ResultType.RECENT; }
We can now create a SearchParameters
constructor with the following method:
private SearchParameters createSearchParam(String searchType, String taste) { SearchParameters.ResultType resultType = getResultType(searchType); SearchParameters searchParameters = new SearchParameters(taste); searchParameters.resultType(resultType); searchParameters.count(3); return searchParameters; }
Now, creating a list of the SearchParameters
constructor is as easy as conducting a map operation (taking a list of keywords and returning a SearchParameters
constructor for each one):
List<SearchParameters> searches = keywords.stream() .map(taste -> createSearchParam(searchType, taste)) .collect(Collectors.toList());
Now, we want to fetch the tweets for each SearchParameters
constructor. You might think of something like this:
List<Tweet> tweets = searches.stream() .map(params -> twitter.searchOperations().search(params)) .map(searchResults -> searchResults.getTweets()) .collect(Collectors.toList());
However, if you think about it, this will return a list of tweets. What we want is to flatten all the tweets to get them as a simple list. It turns out that calling map
and then flattening the result is an operation known as flatMap
. So we can write:
List<Tweet> tweets = searches.stream() .map(params -> twitter.searchOperations().search(params)) .flatMap(searchResults -> searchResults.getTweets().stream()) .collect(Collectors.toList());
The syntax of flatMap
function, that takes a stream as a parameter, is a bit difficult to understand at first. Let me show you the entire code of the SearchService
class so we can take a step back:
package masterSpringMvc.search; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.social.twitter.api.SearchParameters; import org.springframework.social.twitter.api.Tweet; import org.springframework.social.twitter.api.Twitter; import org.springframework.stereotype.Service; import java.util.List; import java.util.stream.Collectors; @Service public class SearchService { private Twitter twitter; @Autowired public SearchService(Twitter twitter) { this.twitter = twitter; } public List<Tweet> search(String searchType, List<String> keywords) { List<SearchParameters> searches = keywords.stream() .map(taste -> createSearchParam(searchType, taste)) .collect(Collectors.toList()); List<Tweet> results = searches.stream() .map(params -> twitter.searchOperations().search(params)) .flatMap(searchResults -> searchResults.getTweets().stream()) .collect(Collectors.toList()); return results; } private SearchParameters.ResultType getResultType(String searchType) { for (SearchParameters.ResultType knownType : SearchParameters.ResultType.values()) { if (knownType.name().equalsIgnoreCase(searchType)) { return knownType; } } return SearchParameters.ResultType.RECENT; } private SearchParameters createSearchParam(String searchType, String taste) { SearchParameters.ResultType resultType = getResultType(searchType); SearchParameters searchParameters = new SearchParameters(taste); searchParameters.resultType(resultType); searchParameters.count(3); return searchParameters; } }
Now, if we navigate to http://localhost:8080/search/mixed;keywords=scala,java
, we get the expected result. A search for the Scala keyword and then for Java: