How to get rid of unnecessary(?) code - adjusting to DRY principle - java

I was with the similiar topic some time ago. I'm looking at my app and I think it has a lot of unnecessary code. What I mean is I have service that is responsible for scraping data from different categories of books from two bookstores. Right now I have 5 categories so I have 5 methods, but what if I'm gonna add some new categories? I will have to add more methods... and I think it is not a good option. Right now it looks like this:
Controller
#GetMapping("/romances")
public Map<Bookstore, List<Book>> get15RomanticBooks() {
return categorizedBookService.get15BooksFromRomanceCategory();
}
#GetMapping("/biographies")
public Map<Bookstore, List<Book>> get15BiographiesBooks() {
return categorizedBookService.get15BooksFromBiographiesCategory();
}
#GetMapping("/guides")
public Map<Bookstore, List<Book>> get15GuidesBooks() {
return categorizedBookService.get15BooksFromGuidesCategory();
}
#GetMapping("/fantasy")
public Map<Bookstore, List<Book>> get15FantasyBooks() {
return categorizedBookService.get15BooksFromFantasyCategory();
}
and here I was thinking about
#GetMapping("/{category}")
public Map<......> get 15BooksFromCategory(#PathVariable CategoryType category)
{...}
I think this is the best way, but it is harder with service.
Service for it looks like this:
package bookstore.scraper.book.scrapingtypeservice;
import bookstore.scraper.enums.Bookstore;
import bookstore.scraper.book.Book;
import bookstore.scraper.fetcher.empik.EmpikFetchingBookService;
import bookstore.scraper.fetcher.merlin.MerlinFetchingBookService;
import bookstore.scraper.urlproperties.EmpikUrlProperties;
import bookstore.scraper.urlproperties.MerlinUrlProperties;
import bookstore.scraper.utilities.JSoupConnector;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;
#Service
#Slf4j
public class CategorizedBookService {
private final EmpikFetchingBookService empikBookService;
private final MerlinFetchingBookService merlinFetchingBookService;
private final EmpikUrlProperties empikUrlProperties;
private final MerlinUrlProperties merlinUrlProperties;
private final JSoupConnector jSoupConnector;
#Autowired
public CategorizedBookService(EmpikFetchingBookService empikBookService, MerlinFetchingBookService merlinFetchingBookService, EmpikUrlProperties empikUrlProperties, MerlinUrlProperties merlinUrlProperties, JSoupConnector jSoupConnector) {
this.empikBookService = empikBookService;
this.merlinFetchingBookService = merlinFetchingBookService;
this.empikUrlProperties = empikUrlProperties;
this.merlinUrlProperties = merlinUrlProperties;
this.jSoupConnector = jSoupConnector;
}
public Map<Bookstore, List<Book>> get15BooksFromRomanceCategory() {
return get15BooksFrom(empikUrlProperties.getEmpik().getRomances(), merlinUrlProperties.getMerlin().getRomances());
}
public Map<Bookstore, List<Book>> get15BooksFromFantasyCategory() {
return get15BooksFrom(empikUrlProperties.getEmpik().getFantasy(), merlinUrlProperties.getMerlin().getFantasy());
}
public Map<Bookstore, List<Book>> get15BooksFromCrimeCategory() {
return get15BooksFrom(empikUrlProperties.getEmpik().getCrime(), merlinUrlProperties.getMerlin().getCrime());
}
public Map<Bookstore, List<Book>> get15BooksFromGuidesCategory() {
return get15BooksFrom(empikUrlProperties.getEmpik().getGuides(), merlinUrlProperties.getMerlin().getGuides());
}
public Map<Bookstore, List<Book>> get15BooksFromBiographiesCategory() {
return get15BooksFrom(empikUrlProperties.getEmpik().getBiographies(), merlinUrlProperties.getMerlin().getBiographies());
}
private Map<Bookstore, List<Book>> get15BooksFrom(String bookStoreEmpikURL, String bookStoreMerlinURL) {
Map<Bookstore, List<Book>> bookstoreWith15CategorizedBooks = new EnumMap<>(Bookstore.class);
bookstoreWith15CategorizedBooks.put(Bookstore.EMPIK, empikBookService
.get15BooksFromCategory(jSoupConnector.connect(bookStoreEmpikURL)));
bookstoreWith15CategorizedBooks.put(Bookstore.MERLIN, merlinFetchingBookService
.get15BooksFromCategory(jSoupConnector.connect(bookStoreMerlinURL)));
return bookstoreWith15CategorizedBooks;
}
}
I have to pass 2 different links depend on which category was called. Is there any way to do this?
EmpikBookService/merlinFetchingBookService are services that use Jsoup to scrape data.
package bookstore.scraper.fetcher.empik;
import bookstore.scraper.book.Book;
import bookstore.scraper.urlproperties.EmpikUrlProperties;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.IntStream;
#Service
public class EmpikFetchingBookService {
private static final int FIRST_PART_PRICE = 0;
private static final int SECOND_PART_PRICE = 1;
private static final int BESTSELLERS_NUMBER_TO_FETCH = 5;
private static final int CATEGORIZED_BOOKS_NUMBER_TO_FETCH = 15;
private static final String DIV_PRODUCT_WRAPPER = "div.productWrapper";
private static final String DATA_PRODUCT_ID = "data-product-id";
private final EmpikUrlProperties empikUrlProperties;
#Autowired
public EmpikFetchingBookService(EmpikUrlProperties empikUrlProperties) {
this.empikUrlProperties = empikUrlProperties;
}
public Book getMostPreciseEmpikBook(Document document) {
String author = document.select("div.smartAuthorWrapper.ta-product-smartauthor").select("a").first().text();
String price = convertEmpikPriceWithPossibleDiscountToActualPrice(document.select("div.price.ta-price-tile").first().text());
String title = document.select(DIV_PRODUCT_WRAPPER).select("strong").first().text();
String productID = document.select(DIV_PRODUCT_WRAPPER).select("a").first().attr(DATA_PRODUCT_ID);
String bookUrl = createBookURL(title, productID);
return Book.builder()
.author(author)
.price(price)
.title(title)
.productID(productID)
.bookURL(bookUrl).build();
}
public List<Book> get5BestSellersEmpik(Document document) {
List<Element> siteElements = document.select(DIV_PRODUCT_WRAPPER);
List<Book> empikBestSellers = new ArrayList<>();
IntStream.range(0, BESTSELLERS_NUMBER_TO_FETCH)
.forEach(iteratedElement -> {
String author = siteElements.get(iteratedElement).select("div.smartAuthorWrapper.ta-product-smartauthor").select("a").first().text();
String price = convertEmpikPriceWithPossibleDiscountToActualPrice(siteElements.get(iteratedElement).select("div.price.ta-price-tile").first().text());
String title = siteElements.get(iteratedElement).select("strong").first().ownText();
String productID = siteElements.get(iteratedElement).select(DIV_PRODUCT_WRAPPER).select("a").first().attr(DATA_PRODUCT_ID);
String bookUrl = createBookURL(title, productID);
empikBestSellers.add(Book.builder()
.author(author)
.price(price)
.title(title)
.productID(productID)
.bookURL(bookUrl)
.build());
});
return empikBestSellers;
}
public List<Book> get15BooksFromCategory(Document document) {
List<Book> books = new ArrayList<>();
List<Element> siteElements = document.select("div.productBox__info");
IntStream.range(0, CATEGORIZED_BOOKS_NUMBER_TO_FETCH)
.forEach(iteratedElement -> {
String author = executeFetchingAuthorProcess(siteElements, iteratedElement);
String price = convertEmpikPriceWithPossibleDiscountToActualPrice(siteElements.get(iteratedElement).select("div.productBox__price").first().text());
String title = siteElements.get(iteratedElement).select("span").first().ownText();
String productID = siteElements.get(iteratedElement).select("a").first().attr(DATA_PRODUCT_ID);
String bookUrl = createBookURL(title, productID);
books.add(Book.builder()
.author(author)
.price(price)
.title(title)
.productID(productID)
.bookURL(bookUrl)
.build());
});
return books;
}
private String convertEmpikPriceWithPossibleDiscountToActualPrice(String price) {
String[] splittedElements = price.split("\\s+");
return splittedElements[FIRST_PART_PRICE] + splittedElements[SECOND_PART_PRICE];
}
private String createBookURL(String title, String productID) {
return String.format(empikUrlProperties.getEmpik().getConcreteBook(), title, productID);
}
//method is required as on empik site, sometimes occurs null for author and we need to change code for fetching
private static String executeFetchingAuthorProcess(List<Element> siteElements, int i) {
String author;
Element authorElements = siteElements.get(i).select("span > a").first();
if (authorElements != null)
author = authorElements.ownText();
else
author = siteElements.get(i).select("> span > span").first().text();
return author;
}
}

(1) The name get15BooksFromCategory(CategoryType) is not right: you are hardcoding a number of books to be returned into the method name.
Today you return 15, tomorrow you would need to return 20, on Sundays you may need to return 5, for Andrews you may need to return 50. You get the point.
Consider these signatures.
getAllBooksFromCategory(CategoryType);
getNBooksFromCategory(CategoryType, Integer);
(2) Get rid of these fields in the service.
private final EmpikUrlProperties empikUrlProperties;
private final MerlinUrlProperties merlinUrlProperties;
private final JSoupConnector jSoupConnector;
The first two are parts of EmpikFetchingBookService and MerlinFetchingBookService, respectively. JSoupConnector is a more low-level abstraction and it shouldn't appear at this level. It may reside in a common parent of those book services or be a separate JSoupService that the common parent is dependent on.
(3) Ideally, you should end up with a very simple service which has a single responsibility - to collect books from its sources.
class BookService {
private List<BookServiceSource> sources;
public Map<String, List<Book>> getBooksByCategory(Category category) {
return sources.stream()
.collect(Collectors.toMap(BookServiceSource::getName,
source -> source.getBooksByCategory(category)));
}
}
BookServiceSource has a similar interface as BookService does. However, MerlinSource, as a subclass of BookServiceSource, doesn't the delegate the job to others. Instead, it prepares an URL and gives it to the JSoupService.
The responsibility of a BookServiceSource is to prepare request parameters and transform the result returned from the JSoupService into a List<Book>. Since each bookstore has a different DOM, you need to know how a particular DOM can be mapped into your structure.
interface BookServiceSource {
String getName();
List<Book> getBooksByCategory(Category category);
}
class MerlinSource implements BookServiceSource {
private JSoupService service;
private MerlinUrlProperties properties;
#Override
public String getName() {
return "merlin";
}
#Override
public List<Book> getBooksByCategory(Category category) {
// at this point, we have both
// JSoupService (to make a real request) and
// MerlinUrlProperties (to prepare everything for that request)
}
}
Think of the MerlinUrlProperties as a utility that can give a mapping between the category and a URL to books from that category.
MerlinUrlProperties could be a Map itself if it contains nothing but a bunch of methods that return URLs. The point is you don't have to define a new method for a new category and force everybody who uses your API to change themselves in order to include a new part of the API. With a Map or enum, the interface would be more stable.
Map<String, String> categoryToMarlinURL = new HashMap<>();
categoryToMarlinURL.put("horror", "marlin.com/horror");
categoryToMarlinURL.put("drama", "marlin.com/drama");
You have everything you need:
the category (category),
the URL to that category (categoryToMarlinURL.get(category)),
the service that makes requests (jSoupService.connect(categoryToMarlinURL.get(category))).

Implement the Chain Of Responsibility pattern and allow services to fetch and put the results to the result Map object. Also let the Spring to make some magic with autowiring Services by providing some common interface
public interface FetchingService {
public Map<Bookstore, List<Book>> fetchAndAddToResult(Map<Bookstore, List<Book>> result, CategoryType category);
}
#Service
public class EmpikFetchingBookService implements FetchingService {
// ...
#Override
public Map<Bookstore, List<Book>> fetchAndAddToResult(Map<Bookstore, List<Book>> result, CategoryType category) {
result.put(Bookstore.EMPIK, getListOfBooks(category));
return result;
}
}
#Service
public class MerlinFetchingBookService implements FetchingService {
// ...
#Override
public Map<Bookstore, List<Book>> fetchAndAddToResult(Map<Bookstore, List<Book>> result, CategoryType category) {
result.put(Bookstore.MERLIN, getListOfBooks(category));
return result;
}
}
#Service
#Slf4j
public class CategorizedBookService {
private final List<FetchingService> services;
//JSoup connector and Properties move to FetchingServices because it is part of those services implementation
#Autowired
public CategorizedBookService(List<FetchingService> services) {
this.services = services;
}
public Map<Bookstore, List<Book>> get15BooksByCategory(CategoryType category) {
Map<Bookstore, List<Book>> result = new HashMap<>();
for(FetchingService service : services) {
result = service.fetchAndAddToResult(result, category);
}
return result;
}
}

1) Split into two different services.
2) I believe it worth to use pagination instead of get15 methods.
3) Instead of having getRomantic , getCrime you can have:
class Service {
private final Map<String,String> categoryToUrl = new HashMap<>();
public Service(){
categoryToUrl.put("crime","http://....");
}
... fetchBook(String category) {
String url = categoryToUrl.get(category);
return fetchUsingJsoap(url);
}
}

Related

How to make the main thread finish last

I have a parser that works with the help of Executor.newCachedThreadPool(), and faced the fact that the main thread in which the record to the JSON file is written is executed before the child ones. As a result, we have an empty file...
I know the topic of multithreading rather poorly and can not understand ge wrong. I tried to use the Join () method on the main thread, but in the end the program just hangs up when approaching this part
Main.java
import model.Product;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
public class Main {
public static void main(String[] args) throws InterruptedException {
String rootUrl = "example.com";
System.out.println("Started parsing: " + rootUrl);
long m = System.currentTimeMillis();
HtmlParser htmlParser = new HtmlParser();
List<Product> productList = new CopyOnWriteArrayList<>();
htmlParser.parse(rootUrl, productList);
Printer.printToJson(productList);
System.out.println("Finish: completed in " + ((double) System.currentTimeMillis() - m) / 1000 + " seconds");
}
}
HtmlParser.java
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import ua.bala.model.Product;
import java.io.IOException;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
public class HtmlParser {
private static AtomicInteger httpRequestsCounter = new AtomicInteger(0);
public static AtomicInteger getHttpRequestsCounter() {
return httpRequestsCounter;
}
public void parse(String url, List<Product> productList) {
try {
Document page = getPage(url);
parsePage(page, productList);
} catch (IOException e) {
e.printStackTrace();
}
}
private static Document getPage(String url) throws IOException {
Document document = Jsoup.connect(url).get();
httpRequestsCounter.getAndIncrement();
return document;
}
private void parsePage(Document page, List<Product> productList) {
Elements productElements = page.select("a.dgBQdu");
ExecutorService service = Executors.newCachedThreadPool();
for (Element element: productElements){
service.execute(() -> {
Long articleID = Long.parseLong(element.attr("id"));
String name = "NAME";
String brand = "BRAND";
BigDecimal price = new BigDecimal(BigInteger.ZERO);
Set<String> colors = new HashSet<>();
String url = "https://www.aboutyou.de" + element.attr("href");
Document innerPage;
try {
innerPage = getPage(url);
Element innerElement = innerPage.selectFirst("[data-test-id='BuyBox']");
name = innerElement.selectFirst("div.dZjUXd").text();
brand = innerElement.selectFirst("[data-test-id='BrandLogo']").attr("alt");
colors = new HashSet<>(innerElement.select("span.jlvxcb-1").eachText());
String priceStr = innerElement.selectFirst("div.dWWxvw > span").text().replace("ab ","").replace(" EUR","").replace(",", ".");
price = new BigDecimal(priceStr);
} catch (IOException e) {
e.printStackTrace();
}
Product product = new Product(articleID, name, brand, colors, price, url);
addProduct(product, productList);
});
}
service.shutdown();
}
private synchronized void addProduct(Product product, List<Product> productList){
System.out.println("Product " + product.getID() + " parsed");
System.out.print(product);
productList.add(product);
System.out.printf("Product %d added to list\n%n", product.getID());
}
}
Printer.java
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import model.Product;
import java.io.*;
import java.util.Comparator;
import java.util.List;
public class Printer {
private static final String path = "";
private static final String fileName = "productsOutput";
public static void printToJson(List<Product> products){
products.sort(Comparator.comparing(Product::getID));
System.out.println("Product list start printing to JSON");
try (final Writer writer = new FileWriter(path + fileName + ".json")) {
Gson gson = new GsonBuilder().create();
gson.toJson(products, writer);
System.out.println("Product list printed to JSON");
System.out.printf("Amount of triggered HTTP requests: %s%nAmount of extracted products: %s%n",
HtmlParser.getHttpRequestsCounter(), products.size());
} catch (IOException e) {
e.printStackTrace();
}
}
}
Printer.java
package model;
import lombok.*;
import java.math.BigDecimal;
import java.util.Set;
import java.util.concurrent.atomic.AtomicLong;
#NoArgsConstructor
#Getter
#Setter
public class Product {
private static AtomicLong productsCounter = new AtomicLong(1);
private Long ID;
private Long articleID;
private String name;
private String brand;
private BigDecimal price;
private Set<String> colors;
private String url;
{
ID = productsCounter.getAndIncrement();
}
public Product(Long articleID, String name, String brand, Set<String> colors, BigDecimal price, String url) {
this.articleID = articleID;
this.name = name;
this.brand = brand;
this.price = price;
this.colors = colors;
this.url = url;
}
public static AtomicLong getProductsCounter() {
return productsCounter;
}
#Override
public String toString() {
return String.format("%d\t%d\t%s\t%s\t%s\t%s\t%s\n", ID, articleID, name, brand, price, colors, url);
}
}
One way to get the main thread to wait for the worker threads to finish would be to have HTMLParser return its ExecutorService so that Main.main can call awaitTermination(...) on it.
Or ... if you didn't want to expose the service to the Main class, you could add a "wait until it is done" method to the HTMLParser classes API.
Note: it is probably a bad idea for each call to parsePage to create and then tear down its own thread pool executor service. You should probably create one thread pool for each HTMLParser instance, and reuse it in each parsePage call.
In addition, doing this will make solving your problem with main easier.
There are several ways to overcome this. Using observables, or blocking main thread or using an interface without blocking the main thread. For me, interface would be a good choice. If you are familiar with java interface, you can implement an interface in order to print recent parsed products. Here is how step by step:
Interface class:
public interface ProductsListener {
void onProductsReady(List<Product> products);
}
MainImpl class (NOT Main class itself):
public class MainImpl implements ProductListener {
// When product list loading is done this func will be called
void onProductsRead(List<Product> products) {
Printer.printToJson(productList);
}
}
In Main class:
public class Main {
public static void main(String[] args) throws InterruptedException {
MainImpl listener = new MainImpl();
htmlParser.setProductListener(listener);
// Rest of the code...
}
}
In HtmlParser class:
public class HtmlParser {
private MainImpl productListener;
//...
public void setProductListener(MainImpl listener) {
// Alternatively you can do it in a constructor
productListener = listener;
}
//...
private void parsePage(Document page, List<Product> productList) {
Elements productElements = page.select("a.dgBQdu");
int parseCount = 0;
ExecutorService service = Executors.newCachedThreadPool();
for (Element element: productElements){
service.execute(() -> {
Long articleID = Long.parseLong(element.attr("id"));
String name = "NAME";
String brand = "BRAND";
BigDecimal price = new BigDecimal(BigInteger.ZERO);
Set<String> colors = new HashSet<>();
String url = "https://www.aboutyou.de" + element.attr("href");
Document innerPage;
try {
innerPage = getPage(url);
Element innerElement = innerPage.selectFirst("[data-test-id='BuyBox']");
name = innerElement.selectFirst("div.dZjUXd").text();
brand = innerElement.selectFirst("[data-test-id='BrandLogo']").attr("alt");
colors = new HashSet<>(innerElement.select("span.jlvxcb-1").eachText());
String priceStr = innerElement.selectFirst("div.dWWxvw > span").text().replace("ab ","").replace(" EUR","").replace(",", ".");
price = new BigDecimal(priceStr);
} catch (IOException e) {
e.printStackTrace();
}
Product product = new Product(articleID, name, brand, colors, price, url);
addProduct(product, productList);
parseCount++; // Count each element that has been parsed
// Check if all elements have been parsed
if(parseCount >= productElements.size()) {
// All products are done, notify the listener class
productListener.onProductsReady(productList);
}
});
}
}
Not tested, but the interface logic must work.

Sort by predefined priority order using comparator

I have the following problem to solve. I am using Java.
A restaurant recognizes 3 types of customers: “NEWBIES”, “REGULARS” and “VIPs”. When customers place their orders, all the orders join a queue. However the orders are always served in such a way that VIPs are served before regulars who are served before newbies.
I need a class which could be used to sort the customer orders. In case two customers are of the same type, the orderID should be used to sort them.
How can I sort by order priority based on the customer type using comparator?
Assuming I already have the following class Order
public class Order
{
public static int orderID;
private int tableNumber;
private String[] orderDetails;
private String customerType;
public Order(int tableNumber, String[] orderDetails, String customerType)
{
this.tableNumber = tableNumber;
this.orderDetails = orderDetails;
this.customerType = customerType;
orderID += 1;
}
// get and set methods declared
}
I have implemented the comparator as follows:
import java.util.Comparator;
public class OrderComparator implements Comparator<Order>
{
#Override
public int compare(Order o1, Order o2)
{
if(o1.getType().equals(o2.getType()))
return o1.getOrderID - o2.getOrderID;
else
// How does comparing the customer type text ensure that
// it will be printed in the right order?
return o1.getType().compareTo(o2.getType());
}
}
Not only do you want to sort on multiple fields, you also want a custom sort with one of those fields.
In the code below, I filled in the missing parts of both class Order and class OrderComparator. Notes after the code.
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
public class Order {
public static final String NEWBIES = "NEWBIES";
public static final String REGULARS = "REGULARS";
public static final String VIP = "VIP";
private static int orderId;
private int orderID;
private int tableNumber;
private String[] orderDetails;
private String customerType;
public Order(int tableNumber, String[] orderDetails, String customerType) {
this.tableNumber = tableNumber;
this.orderDetails = orderDetails;
this.customerType = customerType;
orderID = ++orderId;
}
public int getOrderID() {
return orderID;
}
public int getTableNumber() {
return tableNumber;
}
public String[] getOrderDetails() {
return orderDetails;
}
public String getType() {
return customerType;
}
public String toString() {
return String.format("%d %s", orderID, customerType);
}
public static void main(String[] args) {
Order order1 = new Order(0, null, VIP);
Order order2 = new Order(0, null, REGULARS);
Order order3 = new Order(0, null, REGULARS);
List<Order> list = new ArrayList<>();
list.add(order3);
list.add(order2);
list.add(order1);
System.out.println("Unordered: " + list);
Collections.sort(list, new OrderComparator());
System.out.println("Ordered: " + list);
}
}
class OrderComparator implements Comparator<Order> {
#Override
public int compare(Order o1, Order o2) {
if (o1.getType().equals(o2.getType())) {
return o1.getOrderID() - o2.getOrderID();
}
else {
if (Order.VIP.equals(o1.getType())) {
return -1;
}
else if (Order.VIP.equals(o2.getType())) {
return 1;
}
else if (Order.REGULARS.equals(o1.getType())) {
return -1;
}
else if (Order.REGULARS.equals(o2.getType())) {
return 1;
}
else if (Order.NEWBIES.equals(o1.getType())) {
return -1;
}
else if (Order.NEWBIES.equals(o2.getType())) {
return 1;
}
throw new RuntimeException("Unexpected customer type.");
}
}
}
I added method main to class Order in order to test the code.
I added method toString to class Order so as to be able to check whether the code produces the expected results.
I understand that you want a kind of numerator for Order objects. Hence I made member orderID an instance member since every Order has its own ID and I added a new static member orderId (note that Java is case sensitive) which produces a new, unique order ID for each new Order object.
You want VIP orders to come before REGULARS orders and you want REGULARS orders to come before NEWBIES orders. By default, a Comparator sorts by ascending order, hence you want VIP to be lowest and NEWBIES to be highest (purely for sorting purposes). So in method compare (of class OrderComparator), if, for example, the type of o1 is VIP and the type of o2 is REGULARS then you want VIP to be lower that REGULAR. Hence in that situation, method compare returns -1 (minus one).
Running the above code produces the following output.
Unordered: [3 REGULARS, 2 REGULARS, 1 VIP]
Ordered: [1 VIP, 2 REGULARS, 3 REGULARS]
Note that since customerType (in class Order) is a String, there is a chance that an Order object will be created with an invalid customerType value. You could change the constructor of class Order and add a check for the supplied value (for customerType) and throw an Exception if the supplied value is invalid. Or you could use enum (also known as enumerated types). The below code uses enum instead of String for customerType - which also simplifies method compare in class OrderComparator.
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
public class Order {
private static int orderId;
private int orderID;
private int tableNumber;
private String[] orderDetails;
private CustomerType customerType;
public Order(int tableNumber, String[] orderDetails, CustomerType customerType) {
this.tableNumber = tableNumber;
this.orderDetails = orderDetails;
this.customerType = customerType;
orderID = ++orderId;
}
public int getOrderID() {
return orderID;
}
public int getTableNumber() {
return tableNumber;
}
public String[] getOrderDetails() {
return orderDetails;
}
public CustomerType getType() {
return customerType;
}
public String toString() {
return String.format("%d %s", orderID, customerType);
}
public static void main(String[] args) {
Order order1 = new Order(0, null, CustomerType.VIP);
Order order2 = new Order(0, null, CustomerType.REGULARS);
Order order3 = new Order(0, null, CustomerType.REGULARS);
List<Order> list = new ArrayList<>();
list.add(order3);
list.add(order2);
list.add(order1);
System.out.println("Unordered: " + list);
Collections.sort(list, new OrderComparator());
System.out.println("Ordered: " + list);
}
}
class OrderComparator implements Comparator<Order> {
#Override
public int compare(Order o1, Order o2) {
if (o1.getType().equals(o2.getType())) {
return o1.getOrderID() - o2.getOrderID();
}
else {
return o2.getType().ordinal() - o1.getType().ordinal();
}
}
}
enum CustomerType {
NEWBIES, REGULARS, VIP
}
You can read this question How to sort a collection by multiple fields. Especially the second answer, first option listed.

Spring annotated controller works, but router/handler approach does not appear to retrieve *Mono<>* from *ServerRequest*

Still playing around and trying to understand the "how" of Spring's Webflux and Reactor.
The following successfully adds a new DemoPOJO to the repo when the annotated controller is used (i.e., POST issued at //localhost:8080/v1/DemoPOJO).
However, when issuing the same POST using the router/handler implementation (i.e., //localhost:8080/v2/DemoPOJO), request.bodyToMono(DemoPOJO.class) does not appear to retrieve the DemoPOJO instance from the ServerRequest (i.e., DemoPOJO.printme() is not being invoked).
I'm "working on this", but thought I'd see if anyone can help me "get there faster". For-what-it's-worth, the router/handler implementations (i.e., GET) that don't require getting a DemoPOJO out of ServerRequest are working.
RESTful endpoints using annotation...
#RestController
public class DemoPOJOController {
private Logger logger = LoggerFactory.getLogger(DemoPOJOHandler.class);
#Autowired
DemoPOJOService service;
#RequestMapping(method = POST, value = "/v1/DemoPOJO")
public Mono<Boolean> addDemoPOJO(#RequestBody DemoPOJO demoPOJO) {
logger.debug("DemoPOJOController.addDemoPOJO( {} )", demoPOJO.getId());
return service.add(demoPOJO);
}
}
"Router" part of the corresponding router/handler implementation...
#Configuration
public class DemoPOJORouter {
private Logger logger = LoggerFactory.getLogger(DemoPOJOHandler.class);
#Bean
public RouterFunction<ServerResponse> route(DemoPOJOHandler requestHandler) {
logger.debug("DemoPOJORouter.route( DemoPOJOHandler )");
return nest(path("/v2"),
nest(accept(APPLICATION_JSON),
RouterFunctions.route(RequestPredicates.POST("/DemoPOJO"), requestHandler::add)));
}
}
"Handler" part of the router/handler implementation...
#Component
public class DemoPOJOHandler {
public static final String PATH_VAR_ID = "id";
private Logger logger = LoggerFactory.getLogger(DemoPOJOHandler.class);
#Autowired
private DemoPOJOService service;
public Mono<ServerResponse> add(ServerRequest request) {
logger.debug("DemoPOJOHandler.add( ServerRequest )");
request.bodyToMono(DemoPOJO.class).doOnSuccess(DemoPOJO::printMe);
return ServerResponse.ok().build();
}
}
DemoPOJORepo implementation (hoping to simplify my learning experience by avoiding a "real" repository)...
#Component
public class DemoPOJORepo {
private static final int NUM_OBJS = 15;
private Logger logger = LoggerFactory.getLogger(DemoPOJORepo.class);
private static DemoPOJORepo demoRepo = null;
private Map<Integer, DemoPOJO> demoPOJOMap;
private DemoPOJORepo() {
logger.debug("DemoPOJORepo.DemoPOJORepo()");
initMap();
}
public boolean add(DemoPOJO demoPOJO) {
logger.debug("DemoPOJORepo.add( DemoPOJO )");
boolean pojoAdded = false;
if (!demoPOJOMap.containsKey(demoPOJO.getId())) {
logger.debug("DemoPOJORepo.add( DemoPOJO ) -> adding for id {}", demoPOJO.getId());
demoPOJOMap.put(demoPOJO.getId(), demoPOJO);
pojoAdded = true;
}
return pojoAdded;
}
private void initMap() {
logger.debug("DemoPOJORepo.initMap()");
demoPOJOMap = new TreeMap<Integer, DemoPOJO>();
for (int ndx = 1; ndx < (NUM_OBJS + 1); ndx++) {
demoPOJOMap.put(ndx, new DemoPOJO(ndx, "foo_" + ndx, ndx + 100));
}
}
}
The objects being manipulated...
public class DemoPOJO {
private Logger logger = LoggerFactory.getLogger(DemoPOJOHandler.class);
public static final String DEF_NAME = "DEFAULT NAME";
public static final int DEF_VALUE = 99;
private int id;
private String name;
private int value;
public DemoPOJO(int id) {
this(id, DEF_NAME, DEF_VALUE);
}
public DemoPOJO(#JsonProperty("id") int id, #JsonProperty("name") String name, #JsonProperty("value") int value) {
logger.debug("DemoPOJO.DemoPOJO( {}, {}, {} )", id, name, value);
this.id = id;
this.name = name;
this.value = value;
}
// getters and setters go here
public void printMe() {
logger.debug("DemoPOJO.printMe()");
System.out.printf("id->%d, name->%s, value->%d%n", id, name, value);
}
}
i am guesstimating here since i am writing from mobile. But i think this is your problem.
request.bodyToMono(DemoPOJO.class).doOnSuccess(DemoPOJO::printMe);
return ServerResponse.ok().build();
You are thinking imperative, that first row will be executed then the second which is not the case in webflux. You have to think events-callbacks.
return request.bodyToMono(DemoPOJO.class)
.doOnSuccess(DemoPOJO::printMe)
.thenReturn(ServerResponse.ok().build());
I think this is it but i could be wrong.

In which way I can write down for-loop with List in Java?

Is it possible to use Lambda expression or better way to write down the for-loop?
public TaskDTO convertToDTO(Task task) {
for (int i = 0; i < task.getPrecedingTasks().size(); i++)
this.precedingTasks.add(task.getPrecedingTasks().get(i).getName());
}
Your solution is good as:
task.getPrecedingTasks().stream().map(Task::getName).forEach(this.precedingTasks::add);
But since you are just retrieving the part of the Task, map and then collect as a list as:
this.precedingTasks = task.getPrecedingTasks().stream().map(Task::getName).collect(Collectors.toList());
Isn't it more straightforward and easier to understand? Since stream here is to do the mapping/converting and then collecting.
And also in this way, you don't need to do the initialisation for the this.precedingTasks as
this.precedingTasks = new ArrayList<>(); // to ensure it's not null;
Anyway, just personal preference here.
This is a complete example, where I put System.out.println ... you should use a this.precedingTasks.addAll( ...
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class Main {
public static void main(String[] args) {
Task t1 = new Task("myTask", Arrays.asList(new Task("innerTask1"), new Task("innerTask2")));
System.out.println(t1.precedingTasks.stream().map(Task::getName).collect(Collectors.toList()));
}
static class Task {
private String name;
private List<Task> precedingTasks = new ArrayList<>();
public Task(String name) {
this.name = name;
}
public Task(String name, List<Task> precedingTasks) {
this.name = name;
this.precedingTasks = precedingTasks;
}
public String getName() {
return name;
}
public List<Task> getPrecedingTasks() {
return precedingTasks;
}
}
}
The output is
[innerTask1, innerTask2]
I found the correct solution in my case:
task.getPrecedingTasks().stream().map(Task::getName).forEach(this.precedingTasks::add);
Thanks, for tips : )

Make JsonGetter case insensitive

I'm using JacksonAnnotation along with Spring Framework to parse a JSON that I get from a web service for my app.
I have the same data structure coming from two distinct methods but there is a field in one of them that comes capitalized. I don't want to create two data structures just because of that.
Is there any way that I make JsonGetter case insensitive or at least make it accept two version of a string?
Currently I have to use this for method A
#JsonGetter("CEP")
public String getCEP() {
return this.cep;
}
and this for method B
#JsonGetter("Cep")
public String getCEP() {
return this.cep;
}
Thanks
You can create new setter method for each possibility of property name:
import com.fasterxml.jackson.annotation.JsonSetter;
import com.fasterxml.jackson.databind.ObjectMapper;
public class JacksonProgram {
public static void main(String[] args) throws Exception {
ObjectMapper mapper = new ObjectMapper();
System.out.println(mapper.readValue("{\"Cep\":\"value\"}", Entity.class));
System.out.println(mapper.readValue("{\"CEP\":\"value\"}", Entity.class));
}
}
class Entity {
private String cep;
public String getCep() {
return cep;
}
#JsonSetter("Cep")
public void setCep(String cep) {
this.cep = cep;
}
#JsonSetter("CEP")
public void setCepCapitalized(String cep) {
this.cep = cep;
}
#Override
public String toString() {
return "Entity [cep=" + cep + "]";
}
}

Categories

Resources