--- /dev/null
+package net.wpitchoune.pnews.servlet;
+
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import org.jsoup.Jsoup;
+
+import com.rometools.rome.feed.synd.SyndEnclosure;
+import com.rometools.rome.feed.synd.SyndEntry;
+import com.rometools.rome.feed.synd.SyndFeed;
+import com.rometools.rome.io.FeedException;
+import com.rometools.rome.io.SyndFeedInput;
+import com.rometools.rome.io.XmlReader;
+
+import net.wpitchoune.pnews.Article;
+import net.wpitchoune.pnews.ArticleStore;
+import net.wpitchoune.pnews.Category;
+import net.wpitchoune.pnews.Config;
+import net.wpitchoune.pnews.EntityStat;
+import net.wpitchoune.pnews.Feed;
+import net.wpitchoune.pnews.classifier.NamedEntityRecognizer;
+
+public class ArticleProvider {
+ private static final String CLASS_NAME = ArticleProvider.class.getName();
+ private static final Logger LOG = Logger.getLogger(CLASS_NAME);
+ private final Map<Category, List<Article>> articlesByCategory = new HashMap<>();
+ private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(Runtime.getRuntime().availableProcessors());
+ private final Config config;
+
+ public ArticleProvider(Config config) {
+ this.config = config;
+ for (Category cat: config.getCategories())
+ scheduler.scheduleAtFixedRate(new Refresher(cat), 2, 600, TimeUnit.SECONDS);
+ }
+
+ private static SyndFeed getSyndFeed(String u) throws IllegalArgumentException, FeedException, MalformedURLException, IOException {
+ XmlReader r;
+
+ r = new XmlReader(new URL(u));
+
+ return new SyndFeedInput().build(r);
+ }
+
+ private List<Article> getArticlesForUpdate(Category cat) {
+ List<Article> result;
+
+ synchronized (articlesByCategory) {
+ result = articlesByCategory.get(cat);
+ if (result == null) {
+ result = new ArrayList<>();
+ articlesByCategory.put(cat, result);
+ }
+ return result;
+ }
+ }
+
+ private boolean exists(String articleLink, List<Article> articles) {
+ synchronized (articles) {
+ for (Article a: articles)
+ if (a.getLink().equals(articleLink))
+ return true;
+ }
+ return false;
+ }
+
+ private Instant getArticleInstant(SyndEntry entry) {
+ Date date;
+
+ date = entry.getUpdatedDate();
+ if (date == null)
+ date = entry.getPublishedDate();
+
+ if (date == null)
+ return Instant.now();
+
+ return date.toInstant();
+ }
+
+ private Article toArticle(String link, SyndEntry entry, SyndFeed feed, String lang, Instant instant) {
+ String desc, title, thumbnail, feedTitle, str;
+ List<String> entities;
+
+ feedTitle = feed.getTitle();
+ if (feedTitle != null) {
+ feedTitle = feedTitle.trim();
+ }
+
+ thumbnail = null;
+ for (SyndEnclosure e: entry.getEnclosures()) {
+ if (e.getType().startsWith("image/"))
+ thumbnail = e.getUrl();
+ break;
+ }
+
+ title = entry.getTitle().trim();
+
+ if (entry.getDescription() != null) {
+ str = entry.getDescription().getValue();
+ desc = Jsoup.parse(str).text();
+ } else {
+ desc = null;
+ LOG.severe("No description for " + feedTitle + " - " + title);
+ }
+
+ entities = new ArrayList<>();
+ if (lang.equals("en"))
+ try {
+ NamedEntityRecognizer.classify(title, entities, config);
+ if (desc != null)
+ NamedEntityRecognizer.classify(desc, entities, config);
+ } catch (ClassCastException | ClassNotFoundException | IOException e1) {
+ LOG.log(Level.SEVERE, "Cannot classify " + feedTitle, e1);
+ }
+
+ return new Article(link, title, desc, thumbnail, instant, feedTitle, entities.toArray(new String[0]));
+ }
+
+ private void addArticles(Category cat, SyndFeed feed) {
+ String feedTitle;
+ List<Article> articles;
+ Article a;
+
+ feedTitle = feed.getTitle().trim();
+
+ LOG.info("addArticles " + cat.getLabel() + " " + feedTitle + " number of articles: " + feed.getEntries().size());
+
+ for (SyndEntry entry: feed.getEntries()) {
+ String link = entry.getLink().trim();
+ articles = getArticlesForUpdate(cat);
+ if (exists(link, articles)) {
+ LOG.fine("addArticles " + link + " is already present");
+ continue ;
+ }
+
+ final Instant instant = getArticleInstant(entry);
+
+ if (config.isObsolete(instant))
+ continue ;
+
+ a = ArticleStore.singleton.getArticle(link, ()->toArticle(link, entry, feed, cat.getLanguage(), instant));
+
+ synchronized (articles) {
+ articles.add(a);
+
+ Collections.sort(articles, new Comparator<Article>() {
+ @Override
+ public int compare(Article o1, Article o2) {
+ if (o1.getPublicationDate() == o2.getPublicationDate())
+ return 0;
+ if (o1.getPublicationDate() == null)
+ return 1;
+ if (o2.getPublicationDate() == null)
+ return -1;
+ return o2.getPublicationDate().compareTo(o1.getPublicationDate());
+ }
+ });
+ }
+ }
+
+ LOG.info("addArticles done " + cat.getLabel());
+ }
+
+ private void retrieveArticles(Category cat) throws IllegalArgumentException, MalformedURLException, FeedException, IOException {
+ List<Feed> feeds;
+
+ feeds = config.getFeedsByCategory().get(cat);
+
+ if (feeds != null)
+ for (Feed f: feeds)
+ try {
+ addArticles(cat, getSyndFeed(f.getURL()));
+ } catch (Throwable e) {
+ LOG.log(Level.SEVERE,
+ "retrieveArticles failure " + cat.getLabel() + " " + f.toString(),
+ e);
+ }
+ else
+ LOG.severe("No feed for category " + cat);
+ }
+
+ /**
+ * Returns a copy.
+ */
+ public List<Article> getArticles(Category cat, String entity)
+ throws IllegalArgumentException, MalformedURLException, FeedException, IOException {
+ List<Article> articles, result;
+
+ synchronized (articlesByCategory) {
+ articles = getArticlesForUpdate(cat);
+ }
+
+ synchronized (articles) {
+ if (entity == null)
+ return new ArrayList<>(articles);
+
+ result = new ArrayList<>(articles.size());
+ for (Article a: articles)
+ if (a.hasEntity(entity))
+ result.add(a);
+
+ return result;
+ }
+ }
+
+ public List<EntityStat> getEntityStats(Category cat) throws IllegalArgumentException, MalformedURLException, FeedException, IOException {
+ List<Article> articles;
+ Map<String, EntityStat> entities;
+ final String FUNCTION_NAME = "getEntities";
+ EntityStat s;
+ List<EntityStat> stats;
+ Instant minInstant;
+
+ LOG.entering(CLASS_NAME, FUNCTION_NAME, cat);
+
+ articles = getArticles(cat, null);
+
+ minInstant = Instant.now().minus(15, ChronoUnit.DAYS);
+
+ entities = new HashMap<>();
+ for (Article a: articles)
+ if (a.getPublicationDate().isAfter(minInstant) && a.getEntities() != null)
+ for (String e: a.getEntities()) {
+ s = entities.get(e);
+ if (s == null) {
+ s = new EntityStat(e);
+ entities.put(e, s);
+ }
+ s.increment();
+ }
+
+ stats = new ArrayList<>(entities.values());
+ stats.sort(new Comparator<EntityStat>() {
+
+ @Override
+ public int compare(EntityStat o1, EntityStat o2) {
+ return Integer.compare(o2.getCount(), o1.getCount());
+ }
+
+ });
+
+ LOG.exiting(CLASS_NAME, FUNCTION_NAME, stats);
+
+ return stats;
+ }
+
+ private class Refresher implements Runnable {
+ private final Category category;
+
+ public Refresher(Category category) {
+ this.category = category;
+ }
+
+ @Override
+ public void run() {
+ LOG.info("refresher "+ category.getLabel());
+
+ try {
+ retrieveArticles(category);
+ } catch (IllegalArgumentException | FeedException | IOException e) {
+ LOG.log(Level.SEVERE, "refresher failure", e);
+ }
+
+ LOG.info("refresher "+ category.getLabel() + " done");
+ }
+ }
+}
--- /dev/null
+package net.wpitchoune.pnews.servlet;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.util.List;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import com.rometools.rome.io.FeedException;
+
+import net.wpitchoune.pnews.Article;
+import net.wpitchoune.pnews.Category;
+import net.wpitchoune.pnews.Config;
+import net.wpitchoune.pnews.EntityStat;
+import net.wpitchoune.pnews.Language;
+
+public class HTML {
+ private static final String CLASS_NAME= HTML.class.getName();
+ private static final Logger LOG = Logger.getLogger(CLASS_NAME);
+
+ private static void appendA(StringBuffer buf, String child, String href, String cl) {
+ buf.append("<a href='");
+ buf.append(href);
+ buf.append("'");
+ if (cl != null) {
+ buf.append(" class='");
+ buf.append(cl);
+ buf.append('\'');
+ }
+ buf.append('>');
+ buf.append(child);
+ buf.append("</a>");
+ }
+
+ private static void appendDiv(StringBuffer buf, String child) {
+ buf.append("<div>");
+ buf.append(child);
+ buf.append("</div>\n");
+ }
+
+ private static void appendP(StringBuffer buf, String child) {
+ buf.append("<p>");
+ buf.append(child);
+ buf.append("</p>\n");
+ }
+
+ private static void append(StringBuffer buf, Article a) throws UnsupportedEncodingException {
+ buf.append("<div class='article'>\n");
+
+ buf.append("<div class='article-image'>\n");
+ if (a.getThumbnail() != null) {
+ buf.append("<img class='left' src='");
+ buf.append(a.getThumbnail());
+ buf.append("'/>\n");
+ }
+ buf.append("</div>\n");
+
+ buf.append("<div class='article-content'>\n");
+
+ buf.append("<div class='article-title'>\n");
+ appendA(buf, a.getTitle(), "/redirect?url=" + URLEncoder.encode(a.getLink(), "UTF-8"), null);
+ buf.append("</div>\n");
+
+ buf.append("<div class='article-info'>" + a.getWebsite() + " - " + a.getPublicationDate() + "</div>");
+
+ buf.append("<div class='article-description'>\n");
+ if (a.getDescription() != null) {
+ buf.append("<p>");
+ if (a.getDescription().length() < 512) {
+ buf.append(a.getDescription());
+ } else {
+ buf.append(a.getDescription().substring(0, 512));
+ buf.append("[..]");
+ }
+ buf.append("</p>");
+ }
+ buf.append("</div>\n");
+
+ buf.append("</div>\n");
+
+ buf.append("</div>\n");
+ }
+
+ private static void appendMenu(StringBuffer buf, Category catActive, Config cfg) {
+ String cl;
+
+ buf.append("<nav>\n");
+ buf.append("<ul>\n");
+
+ for (Category cat: cfg.getCategories()) {
+ if (!cat.getLanguage().equals(catActive.getLanguage()))
+ continue;
+
+ buf.append("<li>");
+
+ if (cat.equals(catActive))
+ cl = "active";
+ else
+ cl = null;
+
+ appendA(buf, cat.getLabel(), cat.getURL(), cl);
+ buf.append("</li>");
+ }
+
+ for (Language l: cfg.getLanguages())
+ buf.append("<li><a href='" + l.toURL() + "'>" + l.getLabel() + "</a></li>");
+
+ buf.append("</ul>\n");
+
+ buf.append("</nav>\n");
+ }
+
+ private static String toURL(Category catActive, String entity) {
+ try {
+ return catActive.getURL() + "?entity=" + URLEncoder.encode(entity, "UTF-8");
+ } catch (UnsupportedEncodingException e) {
+ LOG.log(Level.SEVERE, "Failed to generate link to entity " + entity, e);
+ return catActive.getURL();
+ }
+ }
+
+ public static String toHTML(List<Article> articles, Category catActive, String entityActive, Config cfg, ArticleProvider provider) {
+ StringBuffer buf;
+ int i;
+ List<EntityStat> entities;
+ String cl;
+
+ buf = new StringBuffer();
+ buf.append("<!DOCTYPE html>\n");
+ buf.append("<html lang='fr'>\n");
+ buf.append("<head>\n");
+ buf.append("<meta charset=\"UTF-8\">\n");
+ buf.append("<link rel='stylesheet' href='/style.css' />\n");
+ buf.append("<title>");
+ buf.append(catActive.getTitle());
+ buf.append(" - PNews</title>\n");
+ buf.append("</head>\n");
+ buf.append("<body>\n");
+
+ appendMenu(buf, catActive, cfg);
+
+ try {
+ entities = provider.getEntityStats(catActive);
+
+ if (entities.size() > 0) {
+ buf.append("<nav>");
+ buf.append("<ul>");
+ i = 0;
+ for (EntityStat s: entities) {
+ buf.append("<li>");
+ if (entityActive != null && s.getEntity().equals(entityActive))
+ cl = "active";
+ else
+ cl = null;
+ appendA(buf, s.getEntity(), toURL(catActive, s.getEntity()), cl);
+ buf.append("</li>\n");
+ i++;
+ if (i > 10)
+ break;
+ }
+ buf.append("</ul>\n");
+ buf.append("</nav>\n");
+ }
+ } catch (IllegalArgumentException | FeedException | IOException e2) {
+ LOG.log(Level.SEVERE, "Failed to get entities", e2);
+ }
+
+ i = 0;
+ for (Article e: articles) {
+ try {
+ append(buf, e);
+ } catch (UnsupportedEncodingException e1) {
+ LOG.log(Level.SEVERE, "Failed to convert article to HTML", e1);
+ }
+ if (i == 100)
+ break;
+ else
+ i++;
+ }
+
+ buf.append("</body>\n");
+ buf.append("</html>\n");
+
+ return buf.toString();
+ }
+}
--- /dev/null
+package net.wpitchoune.pnews.servlet;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonObject;
+import com.rometools.rome.io.FeedException;
+
+import net.wpitchoune.pnews.Article;
+import net.wpitchoune.pnews.ArticleStore;
+import net.wpitchoune.pnews.Category;
+import net.wpitchoune.pnews.Config;
+
+public class JSON {
+ private static final Logger LOG = Logger.getLogger(JSON.class.getName());
+
+ public static String getStats(ArticleProvider provider, Config config) {
+ JsonObject jstats, jreadcounts, jcategories, jmemory;
+ Runtime runtime;
+ List<Article> articles;
+ Article[] allArticles;
+
+ jstats = new JsonObject();
+
+ jstats.addProperty("articles-count", ArticleStore.singleton.size());
+
+ jreadcounts = new JsonObject();
+ jstats.add("read-counts", jreadcounts);
+
+ allArticles = ArticleStore.singleton.getArticles();
+ for (Article a: allArticles)
+ if (a.getReadCount() > 0)
+ jreadcounts.addProperty(a.getLink(), a.getReadCount());
+
+ jcategories = new JsonObject();
+ jstats.add("categories", jcategories);
+
+ for (Category cat: config.getCategories())
+ try {
+ articles = provider.getArticles(cat, null);
+ jcategories.addProperty(cat.getLabel(),
+ articles.size());
+ } catch (IllegalArgumentException | FeedException | IOException e) {
+ LOG.log(Level.SEVERE, "Fail to retrieve articles", e);
+ }
+
+ jmemory = new JsonObject();
+ jstats.add("memory", jmemory);
+
+ runtime = Runtime.getRuntime();
+ jmemory.addProperty("total", runtime.totalMemory());
+ jmemory.addProperty("max", runtime.maxMemory());
+ jmemory.addProperty("free", runtime.freeMemory());
+
+ return new Gson().toJson(jstats);
+ }
+}
--- /dev/null
+package net.wpitchoune.pnews.servlet;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.io.UnsupportedEncodingException;
+import java.io.Writer;
+import java.net.URLDecoder;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import javax.servlet.ServletConfig;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import com.rometools.rome.io.FeedException;
+
+import net.wpitchoune.pnews.Article;
+import net.wpitchoune.pnews.ArticleStore;
+import net.wpitchoune.pnews.Category;
+import net.wpitchoune.pnews.Config;
+import net.wpitchoune.pnews.Language;
+
+public class Pnews extends HttpServlet {
+ private static final String CLASS_NAME = Pnews.class.getName();
+ private static final Logger LOG = Logger.getLogger(Pnews.class.getName());
+ private static final long serialVersionUID = 1L;
+ private ArticleProvider provider;
+ private Config config;
+
+ private static String getQueryParameter(HttpServletRequest rq, String key)
+ throws UnsupportedEncodingException {
+ final String METHOD_NAME="getQueryParameter";
+ String[] params;
+ int idx;
+ String q;
+
+
+ LOG.entering(CLASS_NAME, METHOD_NAME, new Object[] { rq, key} );
+
+ q = rq.getQueryString();
+
+ if (q == null)
+ return null;
+
+ params = q.split("&");
+
+ for (String p: params) {
+ idx = p.indexOf('=');
+
+ if (idx > 1 && p.substring(0, idx).equals(key))
+ return URLDecoder.decode(p.substring(idx + 1), "UTF-8");
+ }
+
+ return null;
+ }
+
+ private static void redirect(HttpServletRequest rq, HttpServletResponse rp) {
+ String redirectURL;
+ Article a;
+
+ LOG.entering(Pnews.class.getName(), "redirect", new Object[] { rq, rp });
+
+ try {
+ redirectURL = getQueryParameter(rq, "url");
+
+ LOG.info("Request redirection to " + redirectURL);
+
+ if (redirectURL != null) {
+ a = ArticleStore.singleton.get(redirectURL);
+ if (a != null)
+ a.incrementReadCount();
+ else
+ LOG.severe("Cannot find the article " + redirectURL);
+
+ rp.setHeader("Location", redirectURL);
+ rp.setStatus(HttpServletResponse.SC_TEMPORARY_REDIRECT);
+ } else {
+ LOG.severe("No redirection URL");
+ rp.setStatus(HttpServletResponse.SC_NOT_FOUND);
+ }
+
+ } catch (UnsupportedEncodingException e) {
+ e.printStackTrace();
+ LOG.log(Level.SEVERE, "redirect failure", e);
+ rp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+ }
+
+ LOG.exiting(Pnews.class.getName(), "redirect");
+ }
+
+ private static void doTemporaryRedirect(String newURL, HttpServletResponse rp) {
+ rp.setHeader("Location", newURL);
+ rp.setStatus(HttpServletResponse.SC_TEMPORARY_REDIRECT);
+ }
+
+ private void writeStats(HttpServletResponse rp) throws IOException {
+ rp.setContentType("application/json;charset=utf-8");
+ rp.setCharacterEncoding("utf-8");
+
+ rp.getWriter().write(JSON.getStats(provider, config));
+ }
+
+
+ private void writeArticles(Category cat, String entity, HttpServletResponse rp) {
+ String html;
+ List<Article> articles;
+
+ try {
+ articles = provider.getArticles(cat, entity);
+ if (articles != null) {
+ html = HTML.toHTML(articles, cat, entity, config, provider);
+ rp.setContentType("text/html;charset=utf-8");
+ rp.getWriter().write(html);
+ rp.setCharacterEncoding("utf-8");
+ } else {
+ LOG.severe("writeArticles cannot retrieve any articles");
+ html = HTML.toHTML(new ArrayList<Article>(), cat, entity, config, provider);
+ rp.setContentType("text/html");
+ rp.getWriter().write(html);
+ }
+ } catch (IOException | IllegalArgumentException | FeedException e) {
+ LOG.log(Level.SEVERE, "writeArticles failure", e);
+ rp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+ }
+ }
+
+ private void copy(InputStream in, Writer writer) throws IOException {
+ Reader r;
+ char[] buf;
+ int n;
+
+ buf = new char[1024];
+ r = new InputStreamReader(in);
+ while ( (n = r.read(buf, 0, buf.length)) != -1)
+ writer.write(buf, 0, n);
+ }
+
+ @Override
+ protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
+ final String METHOD_NAME = "doGet";
+ String path;
+ InputStream in;
+
+ RequesterLog.singleton.writeRequest(req);
+
+ LOG.info("doGet " + req.getRemoteAddr().toString() + " " + req.getRequestURI() + " " + req.getQueryString());
+
+ LOG.info(METHOD_NAME + " queryString=" + req.getQueryString());
+
+ path = req.getPathInfo();
+
+ if (path.equals("/redirect")) {
+ redirect(req, resp);
+ return ;
+ }
+
+ if (path.equals("/style.css")) {
+ try {
+ in = HTML.class.getClassLoader().getResourceAsStream("style.css");
+ copy(in, resp.getWriter());
+ resp.setContentType("text/css");
+
+ return ;
+ } catch (IOException e) {
+ LOG.log(Level.SEVERE, "doGet failure", e);
+ resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+
+ return ;
+ }
+ }
+
+ if (path.equals("/")) {
+ doTemporaryRedirect(config.getDefaultLanguage().toURL(), resp);
+ return ;
+ }
+
+ try {
+ if (path.equals("/stats")) {
+ writeStats(resp);
+ return ;
+ }
+
+ for (Category cat: config.getCategories()) {
+ if (path.equals(cat.getURL())) {
+ writeArticles(cat, getQueryParameter(req, "entity"), resp);
+ return ;
+ }
+ }
+
+ for (Language l: config.getLanguages()) {
+ if (path.equals(l.toURL())) {
+ doTemporaryRedirect(config.getDefaultCategory(l).getURL(), resp);
+ return ;
+ }
+ }
+
+ resp.getWriter().write("Not found " + req.getPathInfo());
+ resp.setStatus(HttpServletResponse.SC_NOT_FOUND);
+ } catch (IOException | RuntimeException e) {
+ LOG.log(Level.SEVERE, "doGet failure", e);
+ resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+ }
+ }
+
+ @Override
+ public void init(ServletConfig cfg) throws ServletException {
+ LOG.info("Pnews servlet init " + cfg.getServletContext().getContextPath());
+
+ config = new Config();
+ try {
+ config.loadConfig();
+ } catch (UnsupportedEncodingException e) {
+ throw new ServletException(e);
+ }
+
+ provider = new ArticleProvider(config);
+ }
+}
--- /dev/null
+package net.wpitchoune.pnews.servlet;
+
+import java.io.BufferedWriter;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.Writer;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import javax.servlet.http.HttpServletRequest;
+
+public class RequesterLog {
+ public static final RequesterLog singleton = new RequesterLog();
+ private Writer writer;
+ private Logger LOG = Logger.getLogger(RequesterLog.class.getName());
+ private SimpleDateFormat dateFormat = new SimpleDateFormat("dd/MMM/yyyy:HH:mm:ss Z", Locale.US);
+
+ public void writeRequest(HttpServletRequest rq) {
+ try {
+ synchronized(this) {
+ if (writer == null)
+ writer = new BufferedWriter(new FileWriter("access.log", true), 1024);
+ }
+
+ synchronized (writer) {
+ writer.write("[" + dateFormat.format(new Date()) + "] ");
+ writer.write(rq.getRemoteAddr() + " " + rq.getRequestURI() + " " + rq.getQueryString());
+ writer.write(" " + rq.getHeader("User-Agent"));
+ writer.write("\n");
+ writer.flush();
+ }
+ } catch (IOException e) {
+ LOG.log(Level.SEVERE, "Fails to log requester information", e);
+ }
+ }
+}
+++ /dev/null
-package pnews.servlet;
-
-import java.io.IOException;
-import java.net.MalformedURLException;
-import java.net.URL;
-import java.time.Instant;
-import java.time.temporal.ChronoUnit;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.Date;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.concurrent.Executors;
-import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.TimeUnit;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-
-import org.jsoup.Jsoup;
-
-import com.rometools.rome.feed.synd.SyndEnclosure;
-import com.rometools.rome.feed.synd.SyndEntry;
-import com.rometools.rome.feed.synd.SyndFeed;
-import com.rometools.rome.io.FeedException;
-import com.rometools.rome.io.SyndFeedInput;
-import com.rometools.rome.io.XmlReader;
-
-import net.wpitchoune.pnews.Article;
-import net.wpitchoune.pnews.ArticleStore;
-import net.wpitchoune.pnews.Category;
-import net.wpitchoune.pnews.Config;
-import net.wpitchoune.pnews.EntityStat;
-import net.wpitchoune.pnews.Feed;
-import net.wpitchoune.pnews.classifier.NamedEntityRecognizer;
-
-public class ArticleProvider {
- private static final String CLASS_NAME = ArticleProvider.class.getName();
- private static final Logger LOG = Logger.getLogger(CLASS_NAME);
- private final Map<Category, List<Article>> articlesByCategory = new HashMap<>();
- private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(Runtime.getRuntime().availableProcessors());
- private final Config config;
-
- public ArticleProvider(Config config) {
- this.config = config;
- for (Category cat: config.getCategories())
- scheduler.scheduleAtFixedRate(new Refresher(cat), 2, 600, TimeUnit.SECONDS);
- }
-
- private static SyndFeed getSyndFeed(String u) throws IllegalArgumentException, FeedException, MalformedURLException, IOException {
- XmlReader r;
-
- r = new XmlReader(new URL(u));
-
- return new SyndFeedInput().build(r);
- }
-
- private List<Article> getArticlesForUpdate(Category cat) {
- List<Article> result;
-
- synchronized (articlesByCategory) {
- result = articlesByCategory.get(cat);
- if (result == null) {
- result = new ArrayList<>();
- articlesByCategory.put(cat, result);
- }
- return result;
- }
- }
-
- private boolean exists(String articleLink, List<Article> articles) {
- synchronized (articles) {
- for (Article a: articles)
- if (a.getLink().equals(articleLink))
- return true;
- }
- return false;
- }
-
- private Instant getArticleInstant(SyndEntry entry) {
- Date date;
-
- date = entry.getUpdatedDate();
- if (date == null)
- date = entry.getPublishedDate();
-
- if (date == null)
- return Instant.now();
-
- return date.toInstant();
- }
-
- private Article toArticle(String link, SyndEntry entry, SyndFeed feed, String lang, Instant instant) {
- String desc, title, thumbnail, feedTitle, str;
- List<String> entities;
-
- feedTitle = feed.getTitle();
- if (feedTitle != null) {
- feedTitle = feedTitle.trim();
- }
-
- thumbnail = null;
- for (SyndEnclosure e: entry.getEnclosures()) {
- if (e.getType().startsWith("image/"))
- thumbnail = e.getUrl();
- break;
- }
-
- title = entry.getTitle().trim();
-
- if (entry.getDescription() != null) {
- str = entry.getDescription().getValue();
- desc = Jsoup.parse(str).text();
- } else {
- desc = null;
- LOG.severe("No description for " + feedTitle + " - " + title);
- }
-
- entities = new ArrayList<>();
- if (lang.equals("en"))
- try {
- NamedEntityRecognizer.classify(title, entities, config);
- if (desc != null)
- NamedEntityRecognizer.classify(desc, entities, config);
- } catch (ClassCastException | ClassNotFoundException | IOException e1) {
- LOG.log(Level.SEVERE, "Cannot classify " + feedTitle, e1);
- }
-
- return new Article(link, title, desc, thumbnail, instant, feedTitle, entities.toArray(new String[0]));
- }
-
- private void addArticles(Category cat, SyndFeed feed) {
- String feedTitle;
- List<Article> articles;
- Article a;
-
- feedTitle = feed.getTitle().trim();
-
- LOG.info("addArticles " + cat.getLabel() + " " + feedTitle + " number of articles: " + feed.getEntries().size());
-
- for (SyndEntry entry: feed.getEntries()) {
- String link = entry.getLink().trim();
- articles = getArticlesForUpdate(cat);
- if (exists(link, articles)) {
- LOG.fine("addArticles " + link + " is already present");
- continue ;
- }
-
- final Instant instant = getArticleInstant(entry);
-
- if (config.isObsolete(instant))
- continue ;
-
- a = ArticleStore.singleton.getArticle(link, ()->toArticle(link, entry, feed, cat.getLanguage(), instant));
-
- synchronized (articles) {
- articles.add(a);
-
- Collections.sort(articles, new Comparator<Article>() {
- @Override
- public int compare(Article o1, Article o2) {
- if (o1.getPublicationDate() == o2.getPublicationDate())
- return 0;
- if (o1.getPublicationDate() == null)
- return 1;
- if (o2.getPublicationDate() == null)
- return -1;
- return o2.getPublicationDate().compareTo(o1.getPublicationDate());
- }
- });
- }
- }
-
- LOG.info("addArticles done " + cat.getLabel());
- }
-
- private void retrieveArticles(Category cat) throws IllegalArgumentException, MalformedURLException, FeedException, IOException {
- List<Feed> feeds;
-
- feeds = config.getFeedsByCategory().get(cat);
-
- if (feeds != null)
- for (Feed f: feeds)
- try {
- addArticles(cat, getSyndFeed(f.getURL()));
- } catch (Throwable e) {
- LOG.log(Level.SEVERE,
- "retrieveArticles failure " + cat.getLabel() + " " + f.toString(),
- e);
- }
- else
- LOG.severe("No feed for category " + cat);
- }
-
- /**
- * Returns a copy.
- */
- public List<Article> getArticles(Category cat, String entity)
- throws IllegalArgumentException, MalformedURLException, FeedException, IOException {
- List<Article> articles, result;
-
- synchronized (articlesByCategory) {
- articles = getArticlesForUpdate(cat);
- }
-
- synchronized (articles) {
- if (entity == null)
- return new ArrayList<>(articles);
-
- result = new ArrayList<>(articles.size());
- for (Article a: articles)
- if (a.hasEntity(entity))
- result.add(a);
-
- return result;
- }
- }
-
- public List<EntityStat> getEntityStats(Category cat) throws IllegalArgumentException, MalformedURLException, FeedException, IOException {
- List<Article> articles;
- Map<String, EntityStat> entities;
- final String FUNCTION_NAME = "getEntities";
- EntityStat s;
- List<EntityStat> stats;
- Instant minInstant;
-
- LOG.entering(CLASS_NAME, FUNCTION_NAME, cat);
-
- articles = getArticles(cat, null);
-
- minInstant = Instant.now().minus(15, ChronoUnit.DAYS);
-
- entities = new HashMap<>();
- for (Article a: articles)
- if (a.getPublicationDate().isAfter(minInstant) && a.getEntities() != null)
- for (String e: a.getEntities()) {
- s = entities.get(e);
- if (s == null) {
- s = new EntityStat(e);
- entities.put(e, s);
- }
- s.increment();
- }
-
- stats = new ArrayList<>(entities.values());
- stats.sort(new Comparator<EntityStat>() {
-
- @Override
- public int compare(EntityStat o1, EntityStat o2) {
- return Integer.compare(o2.getCount(), o1.getCount());
- }
-
- });
-
- LOG.exiting(CLASS_NAME, FUNCTION_NAME, stats);
-
- return stats;
- }
-
- private class Refresher implements Runnable {
- private final Category category;
-
- public Refresher(Category category) {
- this.category = category;
- }
-
- @Override
- public void run() {
- LOG.info("refresher "+ category.getLabel());
-
- try {
- retrieveArticles(category);
- } catch (IllegalArgumentException | FeedException | IOException e) {
- LOG.log(Level.SEVERE, "refresher failure", e);
- }
-
- LOG.info("refresher "+ category.getLabel() + " done");
- }
- }
-}
+++ /dev/null
-package pnews.servlet;
-
-import java.io.IOException;
-import java.io.UnsupportedEncodingException;
-import java.net.URLEncoder;
-import java.util.List;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-
-import com.rometools.rome.io.FeedException;
-
-import net.wpitchoune.pnews.Article;
-import net.wpitchoune.pnews.Category;
-import net.wpitchoune.pnews.Config;
-import net.wpitchoune.pnews.EntityStat;
-import net.wpitchoune.pnews.Language;
-
-public class HTML {
- private static final String CLASS_NAME= HTML.class.getName();
- private static final Logger LOG = Logger.getLogger(CLASS_NAME);
-
- private static void appendA(StringBuffer buf, String child, String href, String cl) {
- buf.append("<a href='");
- buf.append(href);
- buf.append("'");
- if (cl != null) {
- buf.append(" class='");
- buf.append(cl);
- buf.append('\'');
- }
- buf.append('>');
- buf.append(child);
- buf.append("</a>");
- }
-
- private static void appendDiv(StringBuffer buf, String child) {
- buf.append("<div>");
- buf.append(child);
- buf.append("</div>\n");
- }
-
- private static void appendP(StringBuffer buf, String child) {
- buf.append("<p>");
- buf.append(child);
- buf.append("</p>\n");
- }
-
- private static void append(StringBuffer buf, Article a) throws UnsupportedEncodingException {
- buf.append("<div class='article'>\n");
-
- buf.append("<div class='article-image'>\n");
- if (a.getThumbnail() != null) {
- buf.append("<img class='left' src='");
- buf.append(a.getThumbnail());
- buf.append("'/>\n");
- }
- buf.append("</div>\n");
-
- buf.append("<div class='article-content'>\n");
-
- buf.append("<div class='article-title'>\n");
- appendA(buf, a.getTitle(), "/redirect?url=" + URLEncoder.encode(a.getLink(), "UTF-8"), null);
- buf.append("</div>\n");
-
- buf.append("<div class='article-info'>" + a.getWebsite() + " - " + a.getPublicationDate() + "</div>");
-
- buf.append("<div class='article-description'>\n");
- if (a.getDescription() != null) {
- buf.append("<p>");
- if (a.getDescription().length() < 512) {
- buf.append(a.getDescription());
- } else {
- buf.append(a.getDescription().substring(0, 512));
- buf.append("[..]");
- }
- buf.append("</p>");
- }
- buf.append("</div>\n");
-
- buf.append("</div>\n");
-
- buf.append("</div>\n");
- }
-
- private static void appendMenu(StringBuffer buf, Category catActive, Config cfg) {
- String cl;
-
- buf.append("<nav>\n");
- buf.append("<ul>\n");
-
- for (Category cat: cfg.getCategories()) {
- if (!cat.getLanguage().equals(catActive.getLanguage()))
- continue;
-
- buf.append("<li>");
-
- if (cat.equals(catActive))
- cl = "active";
- else
- cl = null;
-
- appendA(buf, cat.getLabel(), cat.getURL(), cl);
- buf.append("</li>");
- }
-
- for (Language l: cfg.getLanguages())
- buf.append("<li><a href='" + l.toURL() + "'>" + l.getLabel() + "</a></li>");
-
- buf.append("</ul>\n");
-
- buf.append("</nav>\n");
- }
-
- private static String toURL(Category catActive, String entity) {
- try {
- return catActive.getURL() + "?entity=" + URLEncoder.encode(entity, "UTF-8");
- } catch (UnsupportedEncodingException e) {
- LOG.log(Level.SEVERE, "Failed to generate link to entity " + entity, e);
- return catActive.getURL();
- }
- }
-
- public static String toHTML(List<Article> articles, Category catActive, String entityActive, Config cfg, ArticleProvider provider) {
- StringBuffer buf;
- int i;
- List<EntityStat> entities;
- String cl;
-
- buf = new StringBuffer();
- buf.append("<!DOCTYPE html>\n");
- buf.append("<html lang='fr'>\n");
- buf.append("<head>\n");
- buf.append("<meta charset=\"UTF-8\">\n");
- buf.append("<link rel='stylesheet' href='/style.css' />\n");
- buf.append("<title>");
- buf.append(catActive.getTitle());
- buf.append(" - PNews</title>\n");
- buf.append("</head>\n");
- buf.append("<body>\n");
-
- appendMenu(buf, catActive, cfg);
-
- try {
- entities = provider.getEntityStats(catActive);
-
- if (entities.size() > 0) {
- buf.append("<nav>");
- buf.append("<ul>");
- i = 0;
- for (EntityStat s: entities) {
- buf.append("<li>");
- if (entityActive != null && s.getEntity().equals(entityActive))
- cl = "active";
- else
- cl = null;
- appendA(buf, s.getEntity(), toURL(catActive, s.getEntity()), cl);
- buf.append("</li>\n");
- i++;
- if (i > 10)
- break;
- }
- buf.append("</ul>\n");
- buf.append("</nav>\n");
- }
- } catch (IllegalArgumentException | FeedException | IOException e2) {
- LOG.log(Level.SEVERE, "Failed to get entities", e2);
- }
-
- i = 0;
- for (Article e: articles) {
- try {
- append(buf, e);
- } catch (UnsupportedEncodingException e1) {
- LOG.log(Level.SEVERE, "Failed to convert article to HTML", e1);
- }
- if (i == 100)
- break;
- else
- i++;
- }
-
- buf.append("</body>\n");
- buf.append("</html>\n");
-
- return buf.toString();
- }
-}
+++ /dev/null
-package pnews.servlet;
-
-import java.io.IOException;
-import java.util.List;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-
-import com.google.gson.Gson;
-import com.google.gson.JsonObject;
-import com.rometools.rome.io.FeedException;
-
-import net.wpitchoune.pnews.Article;
-import net.wpitchoune.pnews.ArticleStore;
-import net.wpitchoune.pnews.Category;
-import net.wpitchoune.pnews.Config;
-
-public class JSON {
- private static final Logger LOG = Logger.getLogger(JSON.class.getName());
-
- public static String getStats(ArticleProvider provider, Config config) {
- JsonObject jstats, jreadcounts, jcategories, jmemory;
- Runtime runtime;
- List<Article> articles;
- Article[] allArticles;
-
- jstats = new JsonObject();
-
- jstats.addProperty("articles-count", ArticleStore.singleton.size());
-
- jreadcounts = new JsonObject();
- jstats.add("read-counts", jreadcounts);
-
- allArticles = ArticleStore.singleton.getArticles();
- for (Article a: allArticles)
- if (a.getReadCount() > 0)
- jreadcounts.addProperty(a.getLink(), a.getReadCount());
-
- jcategories = new JsonObject();
- jstats.add("categories", jcategories);
-
- for (Category cat: config.getCategories())
- try {
- articles = provider.getArticles(cat, null);
- jcategories.addProperty(cat.getLabel(),
- articles.size());
- } catch (IllegalArgumentException | FeedException | IOException e) {
- LOG.log(Level.SEVERE, "Fail to retrieve articles", e);
- }
-
- jmemory = new JsonObject();
- jstats.add("memory", jmemory);
-
- runtime = Runtime.getRuntime();
- jmemory.addProperty("total", runtime.totalMemory());
- jmemory.addProperty("max", runtime.maxMemory());
- jmemory.addProperty("free", runtime.freeMemory());
-
- return new Gson().toJson(jstats);
- }
-}
+++ /dev/null
-package pnews.servlet;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.io.Reader;
-import java.io.UnsupportedEncodingException;
-import java.io.Writer;
-import java.net.URLDecoder;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-
-import javax.servlet.ServletConfig;
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-
-import com.rometools.rome.io.FeedException;
-
-import net.wpitchoune.pnews.Article;
-import net.wpitchoune.pnews.ArticleStore;
-import net.wpitchoune.pnews.Category;
-import net.wpitchoune.pnews.Config;
-import net.wpitchoune.pnews.Language;
-
-public class Pnews extends HttpServlet {
- private static final String CLASS_NAME = Pnews.class.getName();
- private static final Logger LOG = Logger.getLogger(Pnews.class.getName());
- private static final long serialVersionUID = 1L;
- private ArticleProvider provider;
- private Config config;
-
- private static String getQueryParameter(HttpServletRequest rq, String key)
- throws UnsupportedEncodingException {
- final String METHOD_NAME="getQueryParameter";
- String[] params;
- int idx;
- String q;
-
-
- LOG.entering(CLASS_NAME, METHOD_NAME, new Object[] { rq, key} );
-
- q = rq.getQueryString();
-
- if (q == null)
- return null;
-
- params = q.split("&");
-
- for (String p: params) {
- idx = p.indexOf('=');
-
- if (idx > 1 && p.substring(0, idx).equals(key))
- return URLDecoder.decode(p.substring(idx + 1), "UTF-8");
- }
-
- return null;
- }
-
- private static void redirect(HttpServletRequest rq, HttpServletResponse rp) {
- String redirectURL;
- Article a;
-
- LOG.entering(Pnews.class.getName(), "redirect", new Object[] { rq, rp });
-
- try {
- redirectURL = getQueryParameter(rq, "url");
-
- LOG.info("Request redirection to " + redirectURL);
-
- if (redirectURL != null) {
- a = ArticleStore.singleton.get(redirectURL);
- if (a != null)
- a.incrementReadCount();
- else
- LOG.severe("Cannot find the article " + redirectURL);
-
- rp.setHeader("Location", redirectURL);
- rp.setStatus(HttpServletResponse.SC_TEMPORARY_REDIRECT);
- } else {
- LOG.severe("No redirection URL");
- rp.setStatus(HttpServletResponse.SC_NOT_FOUND);
- }
-
- } catch (UnsupportedEncodingException e) {
- e.printStackTrace();
- LOG.log(Level.SEVERE, "redirect failure", e);
- rp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
- }
-
- LOG.exiting(Pnews.class.getName(), "redirect");
- }
-
- private static void doTemporaryRedirect(String newURL, HttpServletResponse rp) {
- rp.setHeader("Location", newURL);
- rp.setStatus(HttpServletResponse.SC_TEMPORARY_REDIRECT);
- }
-
- private void writeStats(HttpServletResponse rp) throws IOException {
- rp.setContentType("application/json;charset=utf-8");
- rp.setCharacterEncoding("utf-8");
-
- rp.getWriter().write(JSON.getStats(provider, config));
- }
-
-
- private void writeArticles(Category cat, String entity, HttpServletResponse rp) {
- String html;
- List<Article> articles;
-
- try {
- articles = provider.getArticles(cat, entity);
- if (articles != null) {
- html = HTML.toHTML(articles, cat, entity, config, provider);
- rp.setContentType("text/html;charset=utf-8");
- rp.getWriter().write(html);
- rp.setCharacterEncoding("utf-8");
- } else {
- LOG.severe("writeArticles cannot retrieve any articles");
- html = HTML.toHTML(new ArrayList<Article>(), cat, entity, config, provider);
- rp.setContentType("text/html");
- rp.getWriter().write(html);
- }
- } catch (IOException | IllegalArgumentException | FeedException e) {
- LOG.log(Level.SEVERE, "writeArticles failure", e);
- rp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
- }
- }
-
- private void copy(InputStream in, Writer writer) throws IOException {
- Reader r;
- char[] buf;
- int n;
-
- buf = new char[1024];
- r = new InputStreamReader(in);
- while ( (n = r.read(buf, 0, buf.length)) != -1)
- writer.write(buf, 0, n);
- }
-
- @Override
- protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
- final String METHOD_NAME = "doGet";
- String path;
- InputStream in;
-
- RequesterLog.singleton.writeRequest(req);
-
- LOG.info("doGet " + req.getRemoteAddr().toString() + " " + req.getRequestURI() + " " + req.getQueryString());
-
- LOG.info(METHOD_NAME + " queryString=" + req.getQueryString());
-
- path = req.getPathInfo();
-
- if (path.equals("/redirect")) {
- redirect(req, resp);
- return ;
- }
-
- if (path.equals("/style.css")) {
- try {
- in = HTML.class.getClassLoader().getResourceAsStream("style.css");
- copy(in, resp.getWriter());
- resp.setContentType("text/css");
-
- return ;
- } catch (IOException e) {
- LOG.log(Level.SEVERE, "doGet failure", e);
- resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
-
- return ;
- }
- }
-
- if (path.equals("/")) {
- doTemporaryRedirect(config.getDefaultLanguage().toURL(), resp);
- return ;
- }
-
- try {
- if (path.equals("/stats")) {
- writeStats(resp);
- return ;
- }
-
- for (Category cat: config.getCategories()) {
- if (path.equals(cat.getURL())) {
- writeArticles(cat, getQueryParameter(req, "entity"), resp);
- return ;
- }
- }
-
- for (Language l: config.getLanguages()) {
- if (path.equals(l.toURL())) {
- doTemporaryRedirect(config.getDefaultCategory(l).getURL(), resp);
- return ;
- }
- }
-
- resp.getWriter().write("Not found " + req.getPathInfo());
- resp.setStatus(HttpServletResponse.SC_NOT_FOUND);
- } catch (IOException | RuntimeException e) {
- LOG.log(Level.SEVERE, "doGet failure", e);
- resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
- }
- }
-
- @Override
- public void init(ServletConfig cfg) throws ServletException {
- LOG.info("Pnews servlet init " + cfg.getServletContext().getContextPath());
-
- config = new Config();
- try {
- config.loadConfig();
- } catch (UnsupportedEncodingException e) {
- throw new ServletException(e);
- }
-
- provider = new ArticleProvider(config);
- }
-}
+++ /dev/null
-package pnews.servlet;
-
-import java.io.BufferedWriter;
-import java.io.FileWriter;
-import java.io.IOException;
-import java.io.Writer;
-import java.text.SimpleDateFormat;
-import java.util.Date;
-import java.util.Locale;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-
-import javax.servlet.http.HttpServletRequest;
-
-public class RequesterLog {
- public static final RequesterLog singleton = new RequesterLog();
- private Writer writer;
- private Logger LOG = Logger.getLogger(RequesterLog.class.getName());
- private SimpleDateFormat dateFormat = new SimpleDateFormat("dd/MMM/yyyy:HH:mm:ss Z", Locale.US);
-
- public void writeRequest(HttpServletRequest rq) {
- try {
- synchronized(this) {
- if (writer == null)
- writer = new BufferedWriter(new FileWriter("access.log", true), 1024);
- }
-
- synchronized (writer) {
- writer.write("[" + dateFormat.format(new Date()) + "] ");
- writer.write(rq.getRemoteAddr() + " " + rq.getRequestURI() + " " + rq.getQueryString());
- writer.write(" " + rq.getHeader("User-Agent"));
- writer.write("\n");
- writer.flush();
- }
- } catch (IOException e) {
- LOG.log(Level.SEVERE, "Fails to log requester information", e);
- }
- }
-}
"CA",
"Read",
"5 :",
- "InfoWorld"
+ "InfoWorld",
+ "Here"
],
"aliases": {
"U.S.": ["United States", "US"],
<display-name>Pnews</display-name>
<servlet>
<servlet-name>pnews</servlet-name>
- <servlet-class>pnews.servlet.Pnews</servlet-class>
+ <servlet-class>net.wpitchoune.pnews.servlet.Pnews</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<mime-mapping>
<extension>css</extension>
<mime-type>text/css</mime-type>
- </mime-mapping>
+ </mime-mapping>
<servlet-mapping>
<servlet-name>pnews</servlet-name>