package net.i2p.router.news; import java.io.BufferedInputStream; import java.util.Collections; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.Writer; import java.util.ArrayList; import java.util.List; import java.util.zip.GZIPInputStream; import java.util.zip.GZIPOutputStream; import net.i2p.I2PAppContext; import net.i2p.data.Base64; import net.i2p.data.DataHelper; import net.i2p.data.Hash; import net.i2p.util.Log; import net.i2p.util.SecureDirectory; import net.i2p.util.SecureFileOutputStream; import org.cybergarage.util.Debug; import org.cybergarage.xml.Node; import org.cybergarage.xml.ParserException; /** * Store and retrieve news entries from disk. * Each entry is stored in a separate file, with the name * derived from the UUID. * * @since 0.9.23 */ class PersistNews { private static final String DIR = "docs/feed/news"; private static final String PFX = "news-"; private static final String SFX = ".xml.gz"; private static final String XML_START = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"; /** * Store each entry. * Old entries are always overwritten, as they may change even without the updated date changing. * * @param entries each one should be "entry" at the root * @return success */ public static boolean store(I2PAppContext ctx, List<Node> entries) { Log log = ctx.logManager().getLog(PersistNews.class); File dir = new SecureDirectory(ctx.getConfigDir(), DIR); if (!dir.exists()) dir.mkdirs(); StringBuilder buf = new StringBuilder(); boolean rv = true; for (Node entry : entries) { Node nid = entry.getNode("id"); if (nid == null) { if (log.shouldWarn()) log.warn("entry without UUID"); continue; } String id = nid.getValue(); if (id == null) { if (log.shouldWarn()) log.warn("entry without UUID"); continue; } String name = idToName(ctx, id); File file = new File(dir, name); Writer out = null; try { out = new OutputStreamWriter(new GZIPOutputStream(new SecureFileOutputStream(file))); out.write(XML_START); XMLParser.toString(buf, entry); out.write(buf.toString()); buf.setLength(0); } catch (IOException ioe) { if (log.shouldWarn()) log.warn("failed store to " + file, ioe); rv = false; } finally { if (out != null) try { out.close(); } catch (IOException ioe) {} } } return rv; } /** * This does not check for any missing values. * Any fields in any NewsEntry may be null. * Content is not sanitized by NewsXMLParser here, do that before storing. * * @return non-null, sorted by updated date, newest first */ public static List<NewsEntry> load(I2PAppContext ctx) { Log log = ctx.logManager().getLog(PersistNews.class); File dir = new File(ctx.getConfigDir(), DIR); List<NewsEntry> rv = new ArrayList<NewsEntry>(); File[] files = dir.listFiles(); if (files == null) return rv; for (File file : files) { String name = file.getName(); if (!name.startsWith(PFX) || !name.endsWith(SFX)) continue; XMLParser parser = new XMLParser(ctx); InputStream in = null; Node node; boolean error = false; try { in = new GZIPInputStream(new FileInputStream(file)); node = parser.parse(in); NewsEntry entry = extract(node); if (entry != null) { rv.add(entry); } else { if (log.shouldWarn()) log.warn("load error from " + file); error = true; } } catch (ParserException pe) { if (log.shouldWarn()) log.warn("load error from " + file, pe); error = true; } catch (IOException ioe) { if (log.shouldWarn()) log.warn("load error from " + file, ioe); error = true; } finally { if (in != null) try { in.close(); } catch (IOException ioe) {} } if (error) file.delete(); } Collections.sort(rv); return rv; } /** * This does not check for any missing values. * Any fields in any NewsEntry may be null. * Content is not sanitized by NewsXMLParser here, do that before storing. * * @return non-null, throws on errors */ private static NewsEntry extract(Node entry) { NewsEntry e = new NewsEntry(); Node n = entry.getNode("title"); if (n != null) { e.title = n.getValue(); if (e.title != null) e.title = e.title.trim(); } n = entry.getNode("link"); if (n != null) { String a = n.getAttributeValue("href"); if (a.length() > 0) e.link = a.trim(); } n = entry.getNode("id"); if (n != null) { e.id = n.getValue(); if (e.id != null) e.id = e.id.trim(); } n = entry.getNode("updated"); if (n != null) { String v = n.getValue(); if (v != null) { long time = RFC3339Date.parse3339Date(v.trim()); if (time > 0) e.updated = time; } } n = entry.getNode("summary"); if (n != null) { e.summary = n.getValue(); if (e.summary != null) e.summary = e.summary.trim(); } n = entry.getNode("author"); if (n != null) { n = n.getNode("name"); if (n != null) { e.authorName = n.getValue(); if (e.authorName != null) e.authorName = e.authorName.trim(); } } n = entry.getNode("content"); if (n != null) { String a = n.getAttributeValue("type"); if (a.length() > 0) e.contentType = a; // now recursively sanitize // and convert everything in the content to string StringBuilder buf = new StringBuilder(256); for (int i = 0; i < n.getNNodes(); i++) { Node sn = n.getNode(i); XMLParser.toString(buf, sn); } e.content = buf.toString(); } return e; } /** * Unused for now, as we don't have any way to remember it's deleted. * * @return success */ public static boolean delete(I2PAppContext ctx, NewsEntry entry) { String id = entry.id; if (id == null) return false; String name = idToName(ctx, id); File dir = new File(ctx.getConfigDir(), DIR); File file = new File(dir, name); return file.delete(); } /** * @param id non-null */ private static String idToName(I2PAppContext ctx, String id) { byte[] bid = DataHelper.getUTF8(id); byte[] hash = new byte[Hash.HASH_LENGTH]; ctx.sha().calculateHash(bid, 0, bid.length, hash, 0); return PFX + Base64.encode(hash) + SFX; } /**** public static void main(String[] args) { if (args.length != 1) { System.err.println("Usage: PersistNews file.xml"); System.exit(1); } I2PAppContext ctx = new I2PAppContext(); Debug.initialize(ctx); XMLParser parser = new XMLParser(ctx); InputStream in = null; try { in = new FileInputStream(args[0]); Node root = parser.parse(in); List<Node> entries = NewsXMLParser.getNodes(root, "entry"); store(ctx, entries); System.out.println("Stored " + entries.size() + " entries"); } catch (ParserException pe) { System.out.println("load error from " + args[0]); pe.printStackTrace(); } catch (IOException ioe) { System.out.println("load error from " + args[0]); ioe.printStackTrace(); } finally { if (in != null) try { in.close(); } catch (IOException ioe) {} } List<NewsEntry> entries = load(ctx); 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 + '\n' + e.content); } } ****/ }