3
\$\begingroup\$

Task

I want to watch the "Recent Posts" section of a blog for changes/new posts but only for specific posts containing a pre-defined word. Afterwards a list should be outputted in the console with how long it is already published.

Code

RSSWatcher.java

import java.io.IOException;
import java.net.URL;
import java.time.Clock;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import org.apache.commons.cli.ParseException;
import org.jsoup.HttpStatusException;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;

public class RSSWatcher
{
  private static final String WMW = "WMW";
  private static final String EMPEROR = "Emperor";
  private static final String WDQK = "WDQK";

  private static final Map<String, String> favorites = new HashMap<>();
  private static final List<RecentPost> recentPosts = new LinkedList<>();

  public static void main(String[] args) throws ParseException, InterruptedException
  {
    createMap();

    if (args.length > 0)
    {
      initializeCLI(args);
    }

    while (true)
    {
      recentPosts.clear();
      getRecentPostsFiltered();

      for (RecentPost recentPost : recentPosts)
      {
        System.out.println(recentPost.getText() + "\t" + getTimeBetween(recentPost.getPublishedDateTime(),
            LocalDateTime.now(Clock.system(ZoneId.of("America/New_York")))));
      }
      System.out.println("-------------------------------------");

      Thread.sleep(60000);
    }
  }

  private static void initializeCLI(String[] args) throws ParseException
  {
    new CommandLineInterface(args);
  }


  private static void getRecentPostsFiltered()
  {
    Document document = Wuxiaworld.getInstance();

    try
    {
      for (Element element : document.getElementById("recent-posts-2").getElementsByTag("li"))
      {
        Elements tempElements = element.getElementsByTag("a");
        String recentPostTitle = tempElements.get(0).text();

        if (recentPostTitle.length() > 0)
        {
          for (String key : favorites.keySet())
          {
            if (recentPostTitle.contains(key))
            {
              recentPosts.add(0, new RecentPost(tempElements.get(0)));
              break;
            }
          }
        }
      }
    }
    catch (NullPointerException e)
    {
      System.out.println("Probably redirect because proxy was not set. Call with proxy options. " + e);
    }

    getPublishedDate();
  }

  private static void getPublishedDate()
  {
    for (RecentPost recentPost : recentPosts)
    {
      try
      {
        String metaPublishedDate;

        Document doc = Jsoup.parse(new URL(recentPost.getURL()), 5000);
        metaPublishedDate = doc.select("meta[property=article:published_time]").attr("content");

        LocalDateTime from = LocalDateTime.parse(metaPublishedDate, DateTimeFormatter.ISO_DATE_TIME);
        recentPost.setPublishedDateTime(from);
      }
      catch (HttpStatusException e)
      {
        // in some cases the URL is not immediately reachable after publishing, returning HTTP error 404
        recentPost.setPublishedDateTime(LocalDateTime.now());
      }
      catch (IOException e)
      {
        System.out.println(e);
      }
    }
  }

  private static String getTimeBetween(LocalDateTime from, LocalDateTime to)
  {
    try
    {
      long hours = ChronoUnit.MINUTES.between(from, to) / 60;
      long minutes = ChronoUnit.MINUTES.between(from, to) % 60;

      if (hours < 0 || minutes < 0)
      {
        return "NOW";
      }

      return String.format("%d:%02d", hours, minutes);
    }
    catch (NullPointerException e)
    {
      System.out.println(e);
    }
    return "";
  }

  private static void createMap()
  {
    favorites.put(WMW, "Warlock of the Magus World");
    favorites.put(EMPEROR, "Emperor Domination");
    favorites.put(WDQK, "Wu Dong Qian Kun");
  }
}

CommandLineInterface

import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.DefaultParser;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;


public class CommandLineInterface
{
  private Options options = new Options();

  private CommandLineInterface() {}

  public CommandLineInterface(String[] args) throws ParseException
  {
    createOptions();
    parseOptions(args);
  }

  private void parseOptions(String[] args) throws ParseException
  {
    CommandLineParser parser = new DefaultParser();
    CommandLine cmd = parser.parse(options, args);

    if (cmd.hasOption("h") && cmd.hasOption("p"))
    {
      System.setProperty("http.proxyHost", cmd.getOptionValue("h"));
      System.out.println("Set http.proxyHost to " + cmd.getOptionValue("h"));
      System.setProperty("http.proxyPort", cmd.getOptionValue("p"));
      System.out.println("Set http.proxyPort to " + cmd.getOptionValue("p"));
    }
    else if ((cmd.hasOption("h") && !cmd.hasOption("p")) || cmd.hasOption("p") && !cmd.hasOption("h"))
    {
      throw new ParseException("When specifying a proxy you need to enter the host and port of it.");
    }
  }

  private void createOptions()
  {
    options.addOption("h", true, "Set HTTP proxy host");
    options.addOption("p", true, "Set HTTP proxy port");
  }
}

Wuxiaworld.java

import java.io.IOException;
import java.net.URL;
import java.util.concurrent.ThreadLocalRandom;

import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;

public class Wuxiaworld
{
  private static final String WUXIAWORLD_URL = "http://www.wuxiaworld.com";
  private static final int CONNECTION_TIMEOUT = 5000;
  private static final int MINIMUM_EXPIRATION_TIME = 60;
  private static final int MAXIMUM_EXPIRATION_TIME = 600;
  private static Wuxiaworld instance = new Wuxiaworld();
  private static long timestamp = System.currentTimeMillis();
  private static int htmlExpirationTime = 60; // in seconds
  private static Document document;

  private Wuxiaworld()
  {
  }

  public static Document getInstance()
  {
    return instance.getWuxiaworld();
  }

  private Document getWuxiaworld()
  {
    long afterTimestamp = System.currentTimeMillis();

    if (((afterTimestamp - timestamp) / 1000) > htmlExpirationTime || document == null)
    {
      try
      {
        document = Jsoup.parse(new URL(WUXIAWORLD_URL), CONNECTION_TIMEOUT);
        setRandomExpirationTime();
        timestamp = afterTimestamp;
        System.out.println("New HTML expiration time: " + htmlExpirationTime + "s");
      }
      catch (IOException e)
      {
        e.printStackTrace();
      }
    }

    return document;
  }

  private void setRandomExpirationTime()
  {
    htmlExpirationTime = ThreadLocalRandom.current().nextInt(MINIMUM_EXPIRATION_TIME, MAXIMUM_EXPIRATION_TIME);
  }
}

RecentPost.java

import java.time.LocalDateTime;

import org.jsoup.nodes.Element;

public class RecentPost
{
  private String URL = "";
  private String text = "";
  private LocalDateTime publishedDateTime;

  RecentPost(Element element)
  {
    URL = element.attr("href");
    text = element.text();
  }

  public LocalDateTime getPublishedDateTime()
  {
    return publishedDateTime;
  }

  public void setPublishedDateTime(LocalDateTime publishedDateTime)
  {
    this.publishedDateTime = publishedDateTime;
  }

  public String getURL()
  {
    return URL;
  }

  public String getText()
  {
    return text;
  }
}

Things to note, my concerns and reasoning

Initially I subscribed to the RSS feed but it is only updated every hour. So I could miss a new update by a maximum of a hour, if I tried to parse the feed and filter. Hence I wrote this console application to check for new updates by parsing the whole site.

As I do not want to query their website every minute I added a "random" expiration time in the Singleton class responsible for querying the website.

To get the time since a new post was added I am doing this by getting the published date of the meta property of the post itself and calculating it based on the timezone the blog is located (America/New_York). Unfortunately, for new chapters the time would be calculated to a negative value, thus I am returning "NOW" instead of the hh:mm.

What I am looking for are general tips on how to improve and what to change to make it better in the sense of maintainability and reusable. In general, I am happy that it is performing the minimum what I want but I have the feeling it is quite quick&dirty programmed.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.