News: connect it all together (ticket #1425):

- Enable new NewsManager to load/store feed items on disk by UUID
 - News items are stored forever, not lost when they are removed from feed
 - News read in once at startup, not at every summary bar refresh
 - Convert old initialNews.xml and news.xml to NewsEntry format
 - Limit display to 2 news items in summary bar, /home and /console
 - New /news page to show all news
This commit is contained in:
zzz
2015-09-15 13:33:29 +00:00
parent a2e38503fe
commit addc9c5ca3
11 changed files with 130 additions and 81 deletions

View File

@ -4,10 +4,13 @@ import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.IOException; import java.io.IOException;
import java.io.Reader; import java.io.Reader;
import java.text.DateFormat;
import java.text.ParseException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
import java.util.TimeZone;
import net.i2p.I2PAppContext; import net.i2p.I2PAppContext;
import net.i2p.app.ClientAppManager; import net.i2p.app.ClientAppManager;
@ -34,7 +37,13 @@ public class NewsManager implements RouterApp {
private final ClientAppManager _cmgr; private final ClientAppManager _cmgr;
private volatile ClientAppState _state = UNINITIALIZED; private volatile ClientAppState _state = UNINITIALIZED;
private List<NewsEntry> _currentNews; private List<NewsEntry> _currentNews;
private NewsMetadata _currentMetadata; // TODO
// Metadata is persisted in the old news.xml format by
// NewsFetcher.outputOldNewsXML() and read in at startup by
// ConsoleUpdateManager.startup() and NewsFetcher.checkForUpdates().
// While running, the UpdateManager keeps the metadata.
// NewsHelper looks at the news.xml timestamp.
//private NewsMetadata _currentMetadata;
public static final String APP_NAME = "news"; public static final String APP_NAME = "news";
private static final String BUNDLE_NAME = "net.i2p.router.news.messages"; private static final String BUNDLE_NAME = "net.i2p.router.news.messages";
@ -93,10 +102,13 @@ public class NewsManager implements RouterApp {
String id = e.id; String id = e.id;
if (id == null) if (id == null)
continue; continue;
String title = e.title;
boolean found = false; boolean found = false;
for (int i = 0; i < _currentNews.size(); i++) { for (int i = 0; i < _currentNews.size(); i++) {
NewsEntry old = _currentNews.get(i); NewsEntry old = _currentNews.get(i);
if (id.equals(old.id)) { // try to prevent dups with those created from old news.xml,
// where the UUID is the title
if (id.equals(old.id) || (title != null && title.equals(old.id))) {
_currentNews.set(i, e); _currentNews.set(i, e);
found = true; found = true;
break; break;
@ -156,7 +168,7 @@ public class NewsManager implements RouterApp {
String newsContent = FileUtil.readTextFile(file.toString(), -1, true); String newsContent = FileUtil.readTextFile(file.toString(), -1, true);
if (newsContent == null || newsContent.equals("")) if (newsContent == null || newsContent.equals(""))
return Collections.emptyList(); return Collections.emptyList();
return parseNews(newsContent); return parseNews(newsContent, false);
} }
private List<NewsEntry> parseInitialNews() { private List<NewsEntry> parseInitialNews() {
@ -171,7 +183,7 @@ public class NewsManager implements RouterApp {
while((len = reader.read(buf)) > 0) { while((len = reader.read(buf)) > 0) {
out.append(buf, 0, len); out.append(buf, 0, len);
} }
List<NewsEntry> rv = parseNews(out.toString()); List<NewsEntry> rv = parseNews(out.toString(), true);
if (!rv.isEmpty()) { if (!rv.isEmpty()) {
rv.get(0).updated = RFC3339Date.parse3339Date("2015-01-01"); rv.get(0).updated = RFC3339Date.parse3339Date("2015-01-01");
} else { } else {
@ -191,7 +203,12 @@ public class NewsManager implements RouterApp {
} }
} }
private List<NewsEntry> parseNews(String newsContent) { /**
* Used for initialNews.xml and news.xml
*
* @param addMissingDiv true for initialNews, false for news.xml
*/
private List<NewsEntry> parseNews(String newsContent, boolean addMissingDiv) {
List<NewsEntry> rv = new ArrayList<NewsEntry>(); List<NewsEntry> rv = new ArrayList<NewsEntry>();
// Parse news content for headings. // Parse news content for headings.
boolean foundEntry = false; boolean foundEntry = false;
@ -205,10 +222,28 @@ public class NewsManager implements RouterApp {
if (newsContent.length() > start + 16 && if (newsContent.length() > start + 16 &&
newsContent.substring(start + 4, start + 6).equals("20") && newsContent.substring(start + 4, start + 6).equals("20") &&
newsContent.substring(start + 14, start + 16).equals(": ")) { newsContent.substring(start + 14, start + 16).equals(": ")) {
// initialNews.xml, or old news.xml from server
entry.updated = RFC3339Date.parse3339Date(newsContent.substring(start + 4, start + 14)); entry.updated = RFC3339Date.parse3339Date(newsContent.substring(start + 4, start + 14));
newsContent = newsContent.substring(start+16); newsContent = newsContent.substring(start+16);
} else { } else {
newsContent = newsContent.substring(start+4); newsContent = newsContent.substring(start+4);
int colon = newsContent.indexOf(": ");
if (colon > 0 && colon <= 10) {
// Parse the format we wrote it out in, in NewsFetcher.outputOldNewsXML()
// Doesn't work if the date has a : in it, but SHORT hopefully does not
DateFormat fmt = DateFormat.getDateInstance(DateFormat.SHORT);
// the router sets the JVM time zone to UTC but saves the original here so we can get it
String systemTimeZone = _context.getProperty("i2p.systemTimeZone");
if (systemTimeZone != null)
fmt.setTimeZone(TimeZone.getTimeZone(systemTimeZone));
try {
Date date = fmt.parse(newsContent.substring(0, colon));
entry.updated = date.getTime();
newsContent = newsContent.substring(colon + 2);
} catch (ParseException pe) {
// can't find date, will be zero
}
}
} }
int end = newsContent.indexOf("</h3>"); int end = newsContent.indexOf("</h3>");
if (end >= 0) { if (end >= 0) {
@ -222,6 +257,10 @@ public class NewsManager implements RouterApp {
entry.content = newsContent.substring(0, end); entry.content = newsContent.substring(0, end);
else else
entry.content = newsContent; entry.content = newsContent;
// initialNews.xml has the <div> before the <h3>, not after, so we lose it...
// add it back.
if (addMissingDiv)
entry.content = "<div>\n" + entry.content;
rv.add(entry); rv.add(entry);
start = end; start = end;
} }

View File

@ -97,10 +97,11 @@ public class NewsXMLParser {
* *
* @param file XML content only. Any su3 or gunzip handling must have * @param file XML content only. Any su3 or gunzip handling must have
* already happened. * already happened.
* @return the root node
* @throws IOException on any parse error * @throws IOException on any parse error
*/ */
public void parse(File file) throws IOException { public Node parse(File file) throws IOException {
parse(new BufferedInputStream(new FileInputStream(file))); return parse(new BufferedInputStream(new FileInputStream(file)));
} }
/** /**
@ -108,15 +109,17 @@ public class NewsXMLParser {
* *
* @param in XML content only. Any su3 or gunzip handling must have * @param in XML content only. Any su3 or gunzip handling must have
* already happened. * already happened.
* @return the root node
* @throws IOException on any parse error * @throws IOException on any parse error
*/ */
public void parse(InputStream in) throws IOException { public Node parse(InputStream in) throws IOException {
_entries = null; _entries = null;
_metadata = null; _metadata = null;
XMLParser parser = new XMLParser(_context); XMLParser parser = new XMLParser(_context);
try { try {
Node root = parser.parse(in); Node root = parser.parse(in);
extract(root); extract(root);
return root;
} catch (ParserException pe) { } catch (ParserException pe) {
throw new I2PParserException(pe); throw new I2PParserException(pe);
} }
@ -352,7 +355,7 @@ public class NewsXMLParser {
* *
* @return non-null * @return non-null
*/ */
static List<Node> getNodes(Node node, String name) { public static List<Node> getNodes(Node node, String name) {
List<Node> rv = new ArrayList<Node>(); List<Node> rv = new ArrayList<Node>();
int count = node.getNNodes(); int count = node.getNNodes();
for (int i = 0; i < count; i++) { for (int i = 0; i < count; i++) {

View File

@ -35,7 +35,7 @@ import org.cybergarage.xml.ParserException;
*/ */
class PersistNews { class PersistNews {
private static final String DIR = "docs/news"; private static final String DIR = "docs/feed/news";
private static final String PFX = "news-"; private static final String PFX = "news-";
private static final String SFX = ".xml.gz"; private static final String SFX = ".xml.gz";
private static final String XML_START = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"; private static final String XML_START = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n";
@ -207,6 +207,8 @@ class PersistNews {
} }
/** /**
* Unused for now, as we don't have any way to remember it's deleted.
*
* @return success * @return success
*/ */
public static boolean delete(I2PAppContext ctx, NewsEntry entry) { public static boolean delete(I2PAppContext ctx, NewsEntry entry) {

View File

@ -21,12 +21,14 @@ import java.util.Map;
import java.util.StringTokenizer; import java.util.StringTokenizer;
import java.util.TimeZone; import java.util.TimeZone;
import net.i2p.app.ClientAppManager;
import net.i2p.crypto.SU3File; import net.i2p.crypto.SU3File;
import net.i2p.crypto.TrustedUpdate; import net.i2p.crypto.TrustedUpdate;
import net.i2p.data.DataHelper; import net.i2p.data.DataHelper;
import net.i2p.router.RouterContext; import net.i2p.router.RouterContext;
import net.i2p.router.RouterVersion; import net.i2p.router.RouterVersion;
import net.i2p.router.news.NewsEntry; import net.i2p.router.news.NewsEntry;
import net.i2p.router.news.NewsManager;
import net.i2p.router.news.NewsMetadata; import net.i2p.router.news.NewsMetadata;
import net.i2p.router.news.NewsXMLParser; import net.i2p.router.news.NewsXMLParser;
import net.i2p.router.util.RFC822Date; import net.i2p.router.util.RFC822Date;
@ -45,6 +47,8 @@ import net.i2p.util.SSLEepGet;
import net.i2p.util.Translate; import net.i2p.util.Translate;
import net.i2p.util.VersionComparator; import net.i2p.util.VersionComparator;
import org.cybergarage.xml.Node;
/** /**
* Task to fetch updates to the news.xml, and to keep * Task to fetch updates to the news.xml, and to keep
* track of whether that has an announcement for a new version. * track of whether that has an announcement for a new version.
@ -475,10 +479,21 @@ class NewsFetcher extends UpdateRunner {
xml = to1; xml = to1;
} }
NewsXMLParser parser = new NewsXMLParser(_context); NewsXMLParser parser = new NewsXMLParser(_context);
parser.parse(xml); Node root = parser.parse(xml);
xml.delete(); xml.delete();
NewsMetadata data = parser.getMetadata(); NewsMetadata data = parser.getMetadata();
List<NewsEntry> entries = parser.getEntries(); List<NewsEntry> entries = parser.getEntries();
// add entries to the news manager
ClientAppManager cmgr = _context.clientAppManager();
if (cmgr != null) {
NewsManager nmgr = (NewsManager) cmgr.getRegisteredApp(NewsManager.APP_NAME);
if (nmgr != null) {
nmgr.addEntries(entries);
List<Node> nodes = NewsXMLParser.getNodes(root, "entry");
nmgr.storeEntries(nodes);
}
}
// store entries and metadata in old news.xml format
String sudVersion = su3.getVersionString(); String sudVersion = su3.getVersionString();
String signingKeyName = su3.getSignerString(); String signingKeyName = su3.getSignerString();
File to3 = new File(_context.getTempDir(), "tmp3-" + _context.random().nextInt() + ".xml"); File to3 = new File(_context.getTempDir(), "tmp3-" + _context.random().nextInt() + ".xml");

View File

@ -20,7 +20,7 @@ import net.i2p.router.news.NewsManager;
public class NewsFeedHelper extends HelperBase { public class NewsFeedHelper extends HelperBase {
private int _start = 0; private int _start = 0;
private int _limit = 3; private int _limit = 2;
/** /**
* @param limit less than or equal to zero means all * @param limit less than or equal to zero means all
@ -62,16 +62,16 @@ public class NewsFeedHelper extends HelperBase {
for (NewsEntry entry : entries) { for (NewsEntry entry : entries) {
if (i++ < start) if (i++ < start)
continue; continue;
buf.append("<h3>"); buf.append("<div class=\"newsentry\"><h3>");
if (entry.updated > 0) { if (entry.updated > 0) {
Date date = new Date(entry.updated); Date date = new Date(entry.updated);
buf.append(fmt.format(date)) buf.append(fmt.format(date))
.append(": "); .append(": ");
} }
buf.append(entry.title) buf.append(entry.title)
.append("</h3>\n") .append("</h3>\n<div class=\"newscontent\">\n")
.append(entry.content) .append(entry.content)
.append("\n"); .append("\n</div></div>\n");
if (i >= start + max) if (i >= start + max)
break; break;
} }

View File

@ -204,37 +204,12 @@ public class NewsHelper extends ContentHelper {
return mgr.getStatus(); return mgr.getStatus();
} }
private static final String BUNDLE_NAME = "net.i2p.router.news.messages";
/** /**
* If we haven't downloaded news yet, use the translated initial news file * If we haven't downloaded news yet, use the translated initial news file
*/ */
@Override @Override
public String getContent() { public String getContent() {
File news = new File(_page); return NewsFeedHelper.getEntries(_context, 0, 2);
if (!news.exists()) {
_page = (new File(_context.getBaseDir(), "docs/initialNews/initialNews.xml")).getAbsolutePath();
// don't use super, translate on-the-fly
Reader reader = null;
try {
char[] buf = new char[512];
StringBuilder out = new StringBuilder(2048);
reader = new TranslateReader(_context, BUNDLE_NAME, new FileInputStream(_page));
int len;
while((len = reader.read(buf)) > 0) {
out.append(buf, 0, len);
}
return out.toString();
} catch (IOException ioe) {
return "";
} finally {
try {
if (reader != null)
reader.close();
} catch (IOException foo) {}
}
}
return super.getContent();
} }
/** /**
@ -312,7 +287,10 @@ public class NewsHelper extends ContentHelper {
buf.append(" <a href=\"/?news=1&amp;consoleNonce=").append(consoleNonce).append("\">") buf.append(" <a href=\"/?news=1&amp;consoleNonce=").append(consoleNonce).append("\">")
.append(Messages.getString("Show news", ctx)); .append(Messages.getString("Show news", ctx));
} }
buf.append("</a>"); buf.append("</a>" +
" - <a href=\"/news\">")
.append(Messages.getString("Show all news", ctx))
.append("</a>");
} }
return buf.toString(); return buf.toString();
} }

View File

@ -26,8 +26,9 @@ import net.i2p.crypto.KeyStoreUtil;
import net.i2p.data.DataHelper; import net.i2p.data.DataHelper;
import net.i2p.jetty.I2PLogger; import net.i2p.jetty.I2PLogger;
import net.i2p.router.RouterContext; import net.i2p.router.RouterContext;
import net.i2p.router.update.ConsoleUpdateManager;
import net.i2p.router.app.RouterApp; import net.i2p.router.app.RouterApp;
import net.i2p.router.news.NewsManager;
import net.i2p.router.update.ConsoleUpdateManager;
import net.i2p.util.Addresses; import net.i2p.util.Addresses;
import net.i2p.util.FileUtil; import net.i2p.util.FileUtil;
import net.i2p.util.I2PAppThread; import net.i2p.util.I2PAppThread;
@ -706,6 +707,8 @@ public class RouterConsoleRunner implements RouterApp {
ConsoleUpdateManager um = new ConsoleUpdateManager(_context, _mgr, null); ConsoleUpdateManager um = new ConsoleUpdateManager(_context, _mgr, null);
um.start(); um.start();
NewsManager nm = new NewsManager(_context, _mgr, null);
nm.startup();
if (PluginStarter.pluginsEnabled(_context)) { if (PluginStarter.pluginsEnabled(_context)) {
t = new I2PAppThread(new PluginStarter(_context), "PluginStarter", true); t = new I2PAppThread(new PluginStarter(_context), "PluginStarter", true);

View File

@ -3,14 +3,20 @@ package net.i2p.router.web;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.Writer; import java.io.Writer;
import java.text.DateFormat;
import java.util.Collections; import java.util.Collections;
import java.util.Date;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.TimeZone;
import net.i2p.app.ClientAppManager;
import net.i2p.crypto.SigType; import net.i2p.crypto.SigType;
import net.i2p.data.DataHelper; import net.i2p.data.DataHelper;
import net.i2p.router.RouterContext; import net.i2p.router.RouterContext;
import net.i2p.router.news.NewsEntry;
import net.i2p.router.news.NewsManager;
import net.i2p.util.PortMapper; import net.i2p.util.PortMapper;
/** /**
@ -604,49 +610,45 @@ public class SummaryBarRenderer {
String consoleNonce = CSSHelper.getNonce(); String consoleNonce = CSSHelper.getNonce();
if (consoleNonce != null) { if (consoleNonce != null) {
// Set up title and pre-headings stuff. // Set up title and pre-headings stuff.
buf.append("<h3><a href=\"/configupdate\">") //buf.append("<h3><a href=\"/configupdate\">")
buf.append("<h3><a href=\"/news\">")
.append(_("News &amp; Updates")) .append(_("News &amp; Updates"))
.append("</a></h3><hr class=\"b\"><div class=\"newsheadings\">\n"); .append("</a></h3><hr class=\"b\"><div class=\"newsheadings\">\n");
// Get news content. // Get news content.
String newsContent = newshelper.getContent(); List<NewsEntry> entries = Collections.emptyList();
if (newsContent != "") { ClientAppManager cmgr = _context.clientAppManager();
if (cmgr != null) {
NewsManager nmgr = (NewsManager) cmgr.getRegisteredApp(NewsManager.APP_NAME);
if (nmgr != null)
entries = nmgr.getEntries();
}
if (!entries.isEmpty()) {
buf.append("<ul>\n"); buf.append("<ul>\n");
// Parse news content for headings. DateFormat fmt = DateFormat.getDateInstance(DateFormat.SHORT);
boolean foundEntry = false; // the router sets the JVM time zone to UTC but saves the original here so we can get it
int start = newsContent.indexOf("<h3>"); String systemTimeZone = _context.getProperty("i2p.systemTimeZone");
while (start >= 0) { if (systemTimeZone != null)
// Add offset to start: fmt.setTimeZone(TimeZone.getTimeZone(systemTimeZone));
// 4 - gets rid of <h3> int i = 0;
// 16 - gets rid of the date as well (assuming form "<h3>yyyy-mm-dd: Foobarbaz...") final int max = 2;
// Don't truncate the "congratulations" in initial news for (NewsEntry entry : entries) {
if (newsContent.length() > start + 16 && buf.append("<li><a href=\"/?news=1&amp;consoleNonce=")
newsContent.substring(start + 4, start + 6).equals("20") && .append(consoleNonce)
newsContent.substring(start + 14, start + 16).equals(": ")) .append("\">");
newsContent = newsContent.substring(start+16, newsContent.length()); if (entry.updated > 0) {
else Date date = new Date(entry.updated);
newsContent = newsContent.substring(start+4, newsContent.length()); buf.append(fmt.format(date))
int end = newsContent.indexOf("</h3>"); .append(": ");
if (end >= 0) {
String heading = newsContent.substring(0, end);
buf.append("<li><a href=\"/?news=1&amp;consoleNonce=")
.append(consoleNonce)
.append("\">")
.append(heading)
.append("</a></li>\n");
foundEntry = true;
} }
start = newsContent.indexOf("<h3>"); buf.append(entry.title)
.append("</a></li>\n");
if (++i >= max)
break;
} }
buf.append("</ul>\n"); buf.append("</ul>\n");
// Set up string containing <a> to show news. //buf.append("<a href=\"/news\">")
String requestURI = _helper.getRequestURI(); // .append(_("Show all news"))
if (requestURI.contains("/home") && !foundEntry) { // .append("</a>\n");
buf.append("<a href=\"/?news=1&amp;consoleNonce=")
.append(consoleNonce)
.append("\">")
.append(_("Show news"))
.append("</a>\n");
}
} else { } else {
buf.append("<center><i>") buf.append("<center><i>")
.append(_("none")) .append(_("none"))

View File

@ -14,5 +14,6 @@
<jsp:useBean class="net.i2p.router.web.NewsFeedHelper" id="feedHelper" scope="request" /> <jsp:useBean class="net.i2p.router.web.NewsFeedHelper" id="feedHelper" scope="request" />
<jsp:setProperty name="feedHelper" property="contextId" value="<%=(String)session.getAttribute(\"i2p.contextId\")%>" /> <jsp:setProperty name="feedHelper" property="contextId" value="<%=(String)session.getAttribute(\"i2p.contextId\")%>" />
<% feedHelper.setLimit(0); %> <% feedHelper.setLimit(0); %>
<div class="fixme" id="fixme">
<jsp:getProperty name="feedHelper" property="entries" /> <jsp:getProperty name="feedHelper" property="entries" />
</div></body></html> </div></div></body></html>

View File

@ -1,3 +1,9 @@
2015-09-15 zzz
* Console:
- Store news feed items separately on disk in XML, like a real feed reader
- Limit display to 2 news items in summary bar, /home and /console
- New /news page to show all news (ticket #1425)
* 2015-09-12 0.9.22 released * 2015-09-12 0.9.22 released
2015-09-11 kytv 2015-09-11 kytv

View File

@ -18,7 +18,7 @@ public class RouterVersion {
/** deprecated */ /** deprecated */
public final static String ID = "Monotone"; public final static String ID = "Monotone";
public final static String VERSION = CoreVersion.VERSION; public final static String VERSION = CoreVersion.VERSION;
public final static long BUILD = 1; public final static long BUILD = 2;
/** for example "-test" */ /** for example "-test" */
public final static String EXTRA = ""; public final static String EXTRA = "";