Creating REST API in spring boot – Part2

We are done with copying necessaries dependencies and basic spring boot project. As described earlier will be using an in memory database of quotes for serving requests. We have already got the list of quotes and category in form of json files. Its Time to load them into memory.

Let’s understand the structure of Quote and Category first

Quote:

JSON:

{
"quote": "You can observe a lot just by watching.",
"author": "Yogi Berra",
"tags": [

]
}

Each quote will have below properties:

quote: text of the quote.

author: author of the quote

tags: list of tags associated with the quote

Let’s create a POJO class for quote

public class Quote {
private String quote;
private String author;
private List<String> tags;
private int id;
//Getters Setters
}

Category:

JSON:

{
"name":"animal-love",
"imagePath":"https://s3.amazonaws.com/vachan/animal-love.jpg",
"tags":["animals","humanity",""]
}

Each Category will have below properties

name: name of the category

imagePath: path of the image for category or topic

tags: these are sub categories. If some one looks quotes for a particular category the quote will be searched for category and its sub-categories

Now Let’s create a POJO class for Category as well. But we will name it as topic

public class Topic {
private String name;
private String imagePath;
private List<String> tags;
//Getters and Setters
}

Now we are done with models for quotes now let’s create a class to load quotes and serve it to users

Below is functionality provided by this class

  1. It will be a singleton class, hence there will be a single static method which will return instance of QuoteDB
  2. getQuote(): to return all available quotes
  3. getAddedTags() to retuern all available tags for the quotes
  4. getTopics() to return all available Topics or categories
  5. getQuotesFromTopic() to return quotes for a topic in paginated way
  6. getQuote() overloaded method to fetch quotes in paginated way

Create a class name QuoteDB and add below code

public class QuoteDB {
private static QuoteDB instance; // static and singleton instance
private static List<Quote> quotes; // This will contain list of all quotes
private static List<Topic> topics; //This will contains all topics

private static Map<String, List<Integer>> topicQuoteMapper = new HashMap<>(); //This will contain a mapping of quotes for a tag or sub-category

private static final Logger LOGGER = Logger.getLogger(QuoteDB.class);

private QuoteDB(){} // We won't give access to others for creation of QuoteDB instance

/**
* Populates the QuoteDB and returns instance of it
*
@return
*/
public static QuoteDB getInstance() {
if(instance == null) {
instance = new QuoteDB();
populateQuotes(); // Loads quotes
}
return instance;
}
}

populateQuotes method will populate not just quotes but topics and the mapping of quotes with tag as well

So first of all we need a way to read json files. We have used a nice library GSON for this purpose, and we have already added this as a dependency to our pom.xml

Let’s create method to read quotes and topics

// Will read and return list of quotes
private static List<Quote> readQuotes() {
//Try with resource block will make sure to close the stream after use
try(Reader fileReader = new InputStreamReader(QuoteDB.class.getResourceAsStream("/quotes.json"))) {
//This defines a type in which GSON tries to parse contents from json file
Type quoteListType = new TypeToken<List<Quote>>() {
}.getType();
// Reads and returns list of quotes
return new Gson().fromJson(fileReader, quoteListType);
} catch (IOException x) {
LOGGER.error(x.getMessage(), x);
}
return Collections.emptyList();
}

//will read and return list of topics
private static List<Topic> readTopics() {
try(Reader fileReader = new InputStreamReader(QuoteDB.class.getResourceAsStream("/category.json"))) {
Type quoteListType = new TypeToken<List<Topic>>() {
}.getType();
return new Gson().fromJson(fileReader, quoteListType);
} catch (IOException x) {
LOGGER.error(x.getMessage(), x);
}
return Collections.emptyList();
}

So both the functions works in the same way. So may refactor them to have a single method with parameters. But I leave it to you.

Next in the line is the method which will map quotes with topic.

The logic is pretty simple we will have a map with key as tag and value as list of index of quote. We will iterate through list of quotes which was read using readQuotes method and again will iterate over its tag. If map already has this tag as key then we add current quote’s index to the list (value of the map for this tag) otherwise we will create a list of integer and add current quote’s index into it and will put current tag as key and this list as value in the map. Here goes the method

private static Map<String, List<Integer>>  mapQuotesWithTopics(List<Quote> quotes) {
//If quote is null return an empty map
if(quotes == null)
return Collections.emptyMap();
//Creates a map. its key will be tag and value list of index of quotes
Map<String, List<Integer>> topicQuoteMapper = new HashMap<>();
// We will iterate through quote's list
for (int i = 0; i < quotes.size(); i++) {
Quote quote = quotes.get(i);
// Iterate through all tags for this quote
for(String tag : quote.getTags()) {
if(topicQuoteMapper.containsKey(tag.toLowerCase())) {
//If map already contains this tag simply add current quote into the list of quotes for this tag
topicQuoteMapper.get(tag.toLowerCase()).add(i);
} else {
//If not, create a list of integer
List<Integer> indexes = new ArrayList<>();
//Add index of current quote into it
indexes.add(i);
//put them into map
topicQuoteMapper.put(tag.toLowerCase(), indexes);
}
}
}
// Return the map
return topicQuoteMapper;
}

Now we have all pieces for populating our DB. Let’s join them to populate the DB. create a method populateQuote which was called from getInstance method

private static void populateQuotes() {
// populates list of quote
quotes = readQuotes();
// assign an index/id to the quote
for (int i = 0; i < quotes.size(); i++) {
quotes.get(i).setId(i);
}
//populates list of topics
topics = readTopics();
LOGGER.debug("Quotes are loaded");
LOGGER.debug("Indexing quotes");
// maps list of quotes's index with tag
topicQuoteMapper = mapQuotesWithTopics(quotes);
LOGGER.debug("quotes are indexed");
}

Now we are done with populating quotes. Let’s create public methods to expose its services to outer world

/**
* Returns all quotes
*
@return
*/
public List<Quote> getQuotes() {
return quotes;
}

/**
* returns all tags
*
@return
*/
public List<String> getAddedTags() {
return new ArrayList(topicQuoteMapper.keySet());
}

/**
* Returns all topics
*
@return
*/
public List<Topic> getTopics() {
return Collections.unmodifiableList(topics);
}

We further need to provide mechanism to fetch quotes by pagination or for a tag

For returning quotes in pages we will take an offset which will say from where to start and count which will say how many quotes is required. We will return quotes either equal or less then count. Here goes the method

public List<Quote> getQuotes(int offset, int count) {
if(offset <0 || count < 1)
throw new IllegalArgumentException("Offset can't be less than 0 and Max item cant be less than 1");
//Create a sub list
List<Quote> subQuotes = new ArrayList<>();

//if offset stays beyond size of the quote's list then return empty list
if(quotes.size() < offset) {
return subQuotes;
}
//iterate the list from offset till given number of count and add quotes to sub list
for (int i = offset; i < count + offset; i++) {
//break if i goes beyond quote's list
if(quotes.size() <= i)
break;
subQuotes.add(quotes.get(i));
}
//return populated list
return subQuotes;
}

You can also use List..subList(int fromIndex, int toIndex); instead of looping through list’s index. I leave it to you

The next in the line is method for fetching quote for a tag.

Since we have already created a map which maps tags with list of quote’s index it won’t be too difficult

public List<Quote> getQuotesFromTopic(String tag, int offset, int maxItem) {

if(Strings.isNullOrEmpty(tag)) {
throw new IllegalArgumentException("Topic can't be null");
}
if(offset <0 || maxItem < 1)
throw new IllegalArgumentException("Offset can't be less than 0 and Max item cant be less than 1");
//Returns list of quote's index for given tag
List<Integer> indexes = topicQuoteMapper.get(tag.toLowerCase());

//if no list found then return an empty list
if(indexes == null)
return Collections.emptyList();

//Create a list of quote
List<Quote> mappedQuotes = new ArrayList<>();
// iterate through indexes and add corresponding quote to the new list
for(Integer index : indexes) {
if(mappedQuotes.size() <= maxItem)
mappedQuotes.add(quotes.get(index));
else
break
;
}
// return the list
return mappedQuotes;
}

This completes our QuoteDB. This class will serve as a backend of our app and it needs to be populated before user can consume our api. Hence we have to populate it just after spring app boots.

In your SpringApplication class (Should be named as <Your App name choosen in initializer>Application) call method QuoteDB.getInstance()

Here goes my Application class

@SpringBootApplication
@EnableFeignClients(basePackages = {"com.kgcorner.vachan.services"})
public class VachanApplication {

public static void main(String[] args) {
SpringApplication.run(VachanApplication.class, args);
QuoteDB.getInstance(); // We won't be needing returned instance just now but it will populate our backend
}

}

Why getInstance() for populating data:

I agree the name suggest that it only returns the instance and not populate it. Its better to extract and the population of data into some init method and let this method return only instance. I leave it to you.

In next part we will create our REST controllers which will be consumed by end users for accessing our contents.

Leave a comment

Your email address will not be published. Required fields are marked *