URL mapping with matrix variables

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 values
  • Map<String, ?>: This handles a case in which each variable has only one value
  • List<?>: This is used if we are interested in a single variable whose name can be configured

In 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:

URL mapping with matrix variables
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.22.61.218