forked from I2P_Developers/i2p.i2p
289 lines
10 KiB
Java
289 lines
10 KiB
Java
package net.i2p.router.news;
|
|
|
|
import java.io.File;
|
|
import java.io.FileInputStream;
|
|
import java.io.IOException;
|
|
import java.io.Reader;
|
|
import java.text.DateFormat;
|
|
import java.text.ParseException;
|
|
import java.util.ArrayList;
|
|
import java.util.Collections;
|
|
import java.util.Date;
|
|
import java.util.List;
|
|
|
|
import net.i2p.I2PAppContext;
|
|
import net.i2p.app.ClientApp;
|
|
import net.i2p.app.ClientAppManager;
|
|
import net.i2p.app.ClientAppState;
|
|
import static net.i2p.app.ClientAppState.*;
|
|
import net.i2p.data.DataHelper;
|
|
import net.i2p.util.FileUtil;
|
|
import net.i2p.util.Log;
|
|
import net.i2p.util.TranslateReader;
|
|
|
|
import org.cybergarage.xml.Node;
|
|
|
|
/**
|
|
* Manage current news.
|
|
* Keeps current entries in memory, and provide methods to
|
|
* add new entries and store them to disk.
|
|
*
|
|
* @since 0.9.23
|
|
*/
|
|
public class NewsManager implements ClientApp {
|
|
|
|
private final I2PAppContext _context;
|
|
private final Log _log;
|
|
private final ClientAppManager _cmgr;
|
|
private volatile ClientAppState _state = UNINITIALIZED;
|
|
private List<NewsEntry> _currentNews;
|
|
// 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";
|
|
private static final String BUNDLE_NAME = "net.i2p.router.news.messages";
|
|
|
|
/**
|
|
* @param args ignored
|
|
*/
|
|
public NewsManager(I2PAppContext ctx, ClientAppManager listener, String[] args) {
|
|
_context = ctx;
|
|
_cmgr = listener;
|
|
_log = ctx.logManager().getLog(NewsManager.class);
|
|
_state = INITIALIZED;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @return non-null, sorted by updated date, newest first
|
|
*/
|
|
public synchronized List<NewsEntry> getEntries() {
|
|
if (!_currentNews.isEmpty())
|
|
return new ArrayList<NewsEntry>(_currentNews);
|
|
// load old news.xml
|
|
if (_log.shouldWarn())
|
|
_log.warn("no real XML, falling back to news.xml");
|
|
List<NewsEntry> rv = parseOldNews();
|
|
if (!rv.isEmpty()) {
|
|
_currentNews = rv;
|
|
// don't save to disk as we don't have the UUIDs so they will be dups ??
|
|
return rv;
|
|
}
|
|
// load and translate initialnews
|
|
// We don't save it to _currentNews, as the language may change
|
|
if (_log.shouldWarn())
|
|
_log.warn("no news.xml, falling back to initialNews");
|
|
return parseInitialNews();
|
|
}
|
|
|
|
/**
|
|
* Store each entry.
|
|
* Old entries are always overwritten, as they may change even without the updated date changing.
|
|
* Does NOT update the NewsEntry list.
|
|
*
|
|
* @param entries each one should be "entry" at the root
|
|
* @return success
|
|
*/
|
|
public synchronized boolean storeEntries(List<Node> entries) {
|
|
return PersistNews.store(_context, entries);
|
|
}
|
|
|
|
/**
|
|
* Add or replace each entry in the list.
|
|
* Does NOT store them to disk.
|
|
*/
|
|
public synchronized void addEntries(List<NewsEntry> entries) {
|
|
for (NewsEntry e : entries) {
|
|
String id = e.id;
|
|
if (id == null)
|
|
continue;
|
|
String title = e.title;
|
|
boolean found = false;
|
|
for (int i = 0; i < _currentNews.size(); i++) {
|
|
NewsEntry old = _currentNews.get(i);
|
|
// 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);
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!found)
|
|
_currentNews.add(e);
|
|
}
|
|
Collections.sort(_currentNews);
|
|
}
|
|
|
|
/////// ClientApp methods
|
|
|
|
/**
|
|
* ClientApp interface
|
|
*/
|
|
public synchronized void startup() {
|
|
changeState(STARTING);
|
|
_currentNews = PersistNews.load(_context);
|
|
if (_log.shouldWarn())
|
|
_log.warn("Initialized with " + _currentNews.size() + " entries");
|
|
changeState(RUNNING);
|
|
if (_cmgr != null)
|
|
_cmgr.register(this);
|
|
}
|
|
|
|
/**
|
|
* ClientApp interface
|
|
* @param args ignored
|
|
*/
|
|
public synchronized void shutdown(String[] args) {
|
|
changeState(STOPPED);
|
|
}
|
|
|
|
public ClientAppState getState() {
|
|
return _state;
|
|
}
|
|
|
|
public String getName() {
|
|
return APP_NAME;
|
|
}
|
|
|
|
public String getDisplayName() {
|
|
return "News Manager";
|
|
}
|
|
|
|
/////// end ClientApp methods
|
|
|
|
private synchronized void changeState(ClientAppState state) {
|
|
_state = state;
|
|
if (_cmgr != null)
|
|
_cmgr.notify(this, state, null, null);
|
|
}
|
|
|
|
private List<NewsEntry> parseOldNews() {
|
|
File file = new File(_context.getConfigDir(), "docs/news.xml");
|
|
String newsContent = FileUtil.readTextFile(file.toString(), -1, true);
|
|
if (newsContent == null || newsContent.equals(""))
|
|
return Collections.emptyList();
|
|
return parseNews(newsContent, false);
|
|
}
|
|
|
|
private List<NewsEntry> parseInitialNews() {
|
|
NewsEntry entry = new NewsEntry();
|
|
File file = new File(_context.getBaseDir(), "docs/initialNews/initialNews.xml");
|
|
Reader reader = null;
|
|
try {
|
|
char[] buf = new char[512];
|
|
StringBuilder out = new StringBuilder(2048);
|
|
reader = new TranslateReader(_context, BUNDLE_NAME, new FileInputStream(file));
|
|
int len;
|
|
while((len = reader.read(buf)) > 0) {
|
|
out.append(buf, 0, len);
|
|
}
|
|
List<NewsEntry> rv = parseNews(out.toString(), true);
|
|
if (!rv.isEmpty()) {
|
|
rv.get(0).updated = RFC3339Date.parse3339Date("2015-01-01");
|
|
} else {
|
|
if (_log.shouldWarn())
|
|
_log.warn("failed to load " + file);
|
|
}
|
|
return rv;
|
|
} catch (IOException ioe) {
|
|
if (_log.shouldWarn())
|
|
_log.warn("failed to load " + file, ioe);
|
|
return Collections.emptyList();
|
|
} finally {
|
|
try {
|
|
if (reader != null)
|
|
reader.close();
|
|
} catch (IOException foo) {}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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>();
|
|
// Parse news content for headings.
|
|
boolean foundEntry = false;
|
|
int start = newsContent.indexOf("<h3>");
|
|
while (start >= 0) {
|
|
NewsEntry entry = new NewsEntry();
|
|
// Add offset to start:
|
|
// 4 - gets rid of <h3>
|
|
// 16 - gets rid of the date as well (assuming form "<h3>yyyy-mm-dd: Foobarbaz...")
|
|
// Don't truncate the "congratulations" in initial news
|
|
if (newsContent.length() > start + 16 &&
|
|
newsContent.substring(start + 4, start + 6).equals("20") &&
|
|
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));
|
|
newsContent = newsContent.substring(start+16);
|
|
} else {
|
|
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
|
|
fmt.setTimeZone(DataHelper.getSystemTimeZone(_context));
|
|
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>");
|
|
if (end >= 0) {
|
|
String heading = newsContent.substring(0, end);
|
|
entry.title = heading;
|
|
// use title as UUID
|
|
entry.id = heading;
|
|
newsContent = newsContent.substring(end + 5);
|
|
end = newsContent.indexOf("<h3>");
|
|
if (end > 0)
|
|
entry.content = newsContent.substring(0, end);
|
|
else
|
|
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);
|
|
start = end;
|
|
}
|
|
}
|
|
Collections.sort(rv);
|
|
return rv;
|
|
}
|
|
|
|
/****
|
|
public static void main(String[] args) {
|
|
if (args.length != 0) {
|
|
System.err.println("Usage: NewsManager");
|
|
System.exit(1);
|
|
}
|
|
I2PAppContext ctx = new I2PAppContext();
|
|
NewsManager mgr = new NewsManager(ctx, null, null);
|
|
mgr.startup();
|
|
List<NewsEntry> entries = mgr.getEntries();
|
|
System.out.println("Loaded " + entries.size() + " news entries");
|
|
for (int i = 0; i < entries.size(); i++) {
|
|
NewsEntry e = entries.get(i);
|
|
System.out.println("\n****** News #" + (i+1) + ": " + e.title + ' ' + new Date(e.updated) +
|
|
"\nLink: " + e.link + '\n' + e.content);
|
|
}
|
|
}
|
|
****/
|
|
}
|