From f6979c811fa51c46907cd0220fc28242e68324fe Mon Sep 17 00:00:00 2001 From: jrandom Date: Fri, 11 Nov 2005 03:41:16 +0000 Subject: [PATCH] 2005-11-10 jrandom * First pass to a new threaded Syndie interface, which isn't enabled by default, as its not done yet. --- .../java/src/net/i2p/syndie/Archive.java | 1 + .../src/net/i2p/syndie/ArchiveIndexer.java | 24 +- .../java/src/net/i2p/syndie/BlogManager.java | 3 +- .../src/net/i2p/syndie/HeaderReceiver.java | 43 + .../src/net/i2p/syndie/ThreadNodeImpl.java | 104 ++ .../net/i2p/syndie/WritableThreadIndex.java | 148 +++ .../src/net/i2p/syndie/data/ArchiveIndex.java | 4 + .../java/src/net/i2p/syndie/data/BlogURI.java | 4 +- .../i2p/syndie/data/FilteredThreadIndex.java | 88 ++ .../src/net/i2p/syndie/data/ThreadIndex.java | 49 + .../src/net/i2p/syndie/data/ThreadNode.java | 34 + .../syndie/data/TransparentArchiveIndex.java | 1 + .../src/net/i2p/syndie/sml/HTMLRenderer.java | 6 +- .../i2p/syndie/web/ViewThreadedServlet.java | 1053 +++++++++++++++++ apps/syndie/jsp/images/addToFavorites.png | Bin 0 -> 275 bytes apps/syndie/jsp/images/addToIgnored.png | Bin 0 -> 266 bytes apps/syndie/jsp/images/collapse.png | Bin 0 -> 917 bytes apps/syndie/jsp/images/expand.png | Bin 0 -> 922 bytes apps/syndie/jsp/images/favorites.png | Bin 0 -> 463 bytes apps/syndie/jsp/images/noSubthread.png | Bin 0 -> 129 bytes apps/syndie/jsp/images/threadIndent.png | Bin 0 -> 129 bytes apps/syndie/jsp/index.html | 3 + apps/syndie/jsp/switchuser.jsp | 16 + apps/syndie/jsp/web.xml | 15 +- 24 files changed, 1586 insertions(+), 10 deletions(-) create mode 100644 apps/syndie/java/src/net/i2p/syndie/HeaderReceiver.java create mode 100644 apps/syndie/java/src/net/i2p/syndie/ThreadNodeImpl.java create mode 100644 apps/syndie/java/src/net/i2p/syndie/WritableThreadIndex.java create mode 100644 apps/syndie/java/src/net/i2p/syndie/data/FilteredThreadIndex.java create mode 100644 apps/syndie/java/src/net/i2p/syndie/data/ThreadIndex.java create mode 100644 apps/syndie/java/src/net/i2p/syndie/data/ThreadNode.java create mode 100644 apps/syndie/java/src/net/i2p/syndie/web/ViewThreadedServlet.java create mode 100644 apps/syndie/jsp/images/addToFavorites.png create mode 100644 apps/syndie/jsp/images/addToIgnored.png create mode 100644 apps/syndie/jsp/images/collapse.png create mode 100644 apps/syndie/jsp/images/expand.png create mode 100644 apps/syndie/jsp/images/favorites.png create mode 100644 apps/syndie/jsp/images/noSubthread.png create mode 100644 apps/syndie/jsp/images/threadIndent.png create mode 100644 apps/syndie/jsp/index.html create mode 100644 apps/syndie/jsp/switchuser.jsp diff --git a/apps/syndie/java/src/net/i2p/syndie/Archive.java b/apps/syndie/java/src/net/i2p/syndie/Archive.java index 0487cabe1..22232e3de 100644 --- a/apps/syndie/java/src/net/i2p/syndie/Archive.java +++ b/apps/syndie/java/src/net/i2p/syndie/Archive.java @@ -244,6 +244,7 @@ public class Archive { } public List listEntries(BlogURI uri, String tag, SessionKey blogKey) { + if (uri == null) return new ArrayList(); return listEntries(uri.getKeyHash(), uri.getEntryId(), tag, blogKey); } public List listEntries(Hash blog, long entryId, String tag, SessionKey blogKey) { diff --git a/apps/syndie/java/src/net/i2p/syndie/ArchiveIndexer.java b/apps/syndie/java/src/net/i2p/syndie/ArchiveIndexer.java index a22b31436..379a390d8 100644 --- a/apps/syndie/java/src/net/i2p/syndie/ArchiveIndexer.java +++ b/apps/syndie/java/src/net/i2p/syndie/ArchiveIndexer.java @@ -19,6 +19,7 @@ class ArchiveIndexer { public static ArchiveIndex index(I2PAppContext ctx, Archive source) { Log log = ctx.logManager().getLog(ArchiveIndexer.class); LocalArchiveIndex rv = new LocalArchiveIndex(ctx); + WritableThreadIndex threads = new WritableThreadIndex(); rv.setGeneratedOn(ctx.clock().now()); File rootDir = source.getArchiveDir(); @@ -79,6 +80,7 @@ class ArchiveIndexer { allEntries++; totalSize += entry.getCompleteSize(); String entryTags[] = entry.getTags(); + threads.addEntry(entry.getURI(), entryTags); for (int t = 0; t < entryTags.length; t++) { if (!tags.containsKey(entryTags[t])) { tags.put(entryTags[t], new TreeMap()); @@ -98,11 +100,18 @@ class ArchiveIndexer { parser.parse(entry.getEntry().getText(), rec); String reply = rec.getHeader(HTMLRenderer.HEADER_IN_REPLY_TO); if (reply != null) { - BlogURI parent = new BlogURI(reply.trim()); - if ( (parent.getKeyHash() != null) && (parent.getEntryId() >= 0) ) - rv.addReply(parent, entry.getURI()); - else if (log.shouldLog(Log.WARN)) - log.warn("Parent of " + entry.getURI() + " is not valid: [" + reply.trim() + "]"); + String forceNewThread = rec.getHeader(HTMLRenderer.HEADER_FORCE_NEW_THREAD); + if ( (forceNewThread != null) && (Boolean.valueOf(forceNewThread).booleanValue()) ) { + // ignore the parent + } else { + BlogURI parent = new BlogURI(reply.trim()); + if ( (parent.getKeyHash() != null) && (parent.getEntryId() >= 0) ) { + rv.addReply(parent, entry.getURI()); + threads.addParent(parent, entry.getURI()); + } else if (log.shouldLog(Log.WARN)) { + log.warn("Parent of " + entry.getURI() + " is not valid: [" + reply.trim() + "]"); + } + } } } @@ -150,6 +159,11 @@ class ArchiveIndexer { rv.addNewestEntry(uri); } + threads.organizeTree(); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Tree: \n" + threads.toString()); + rv.setThreadedIndex(threads); + return rv; } diff --git a/apps/syndie/java/src/net/i2p/syndie/BlogManager.java b/apps/syndie/java/src/net/i2p/syndie/BlogManager.java index 861a24819..5189437c2 100644 --- a/apps/syndie/java/src/net/i2p/syndie/BlogManager.java +++ b/apps/syndie/java/src/net/i2p/syndie/BlogManager.java @@ -44,7 +44,8 @@ public class BlogManager { if (rootDir == null) rootDir = "./syndie"; } - _instance = new BlogManager(I2PAppContext.getGlobalContext(), rootDir); + _instance = new BlogManager(I2PAppContext.getGlobalContext(), rootDir, false); + _instance.getArchive().regenerateIndex(); } return _instance; } diff --git a/apps/syndie/java/src/net/i2p/syndie/HeaderReceiver.java b/apps/syndie/java/src/net/i2p/syndie/HeaderReceiver.java new file mode 100644 index 000000000..9a9c2aa19 --- /dev/null +++ b/apps/syndie/java/src/net/i2p/syndie/HeaderReceiver.java @@ -0,0 +1,43 @@ +package net.i2p.syndie; + +import java.util.*; +import net.i2p.syndie.sml.SMLParser; + +public class HeaderReceiver implements SMLParser.EventReceiver { + private Properties _headers; + public HeaderReceiver() { _headers = null; } + public String getHeader(String name) { return (_headers != null ? _headers.getProperty(name) : null); } + public void receiveHeader(String header, String value) { + if (_headers == null) _headers = new Properties(); + _headers.setProperty(header, value); + } + + public void receiveAddress(String name, String schema, String protocol, String location, String anchorText) {} + public void receiveArchive(String name, String description, String locationSchema, String location, String postingKey, String anchorText) {} + public void receiveAttachment(int id, String anchorText) {} + public void receiveBegin() {} + public void receiveBlog(String name, String blogKeyHash, String blogPath, long blogEntryId, List blogArchiveLocations, String anchorText) {} + public void receiveBold(String text) {} + public void receiveCode(String text, String codeLocationSchema, String codeLocation) {} + public void receiveCut(String summaryText) {} + public void receiveEnd() {} + public void receiveGT() {} + public void receiveH1(String text) {} + public void receiveH2(String text) {} + public void receiveH3(String text) {} + public void receiveH4(String text) {} + public void receiveH5(String text) {} + public void receiveHR() {} + public void receiveHeaderEnd() {} + public void receiveImage(String alternateText, int attachmentId) {} + public void receiveItalic(String text) {} + public void receiveLT() {} + public void receiveLeftBracket() {} + public void receiveLink(String schema, String location, String text) {} + public void receiveNewline() {} + public void receivePlain(String text) {} + public void receivePre(String text) {} + public void receiveQuote(String text, String whoQuoted, String quoteLocationSchema, String quoteLocation) {} + public void receiveRightBracket() {} + public void receiveUnderline(String text) {} +} diff --git a/apps/syndie/java/src/net/i2p/syndie/ThreadNodeImpl.java b/apps/syndie/java/src/net/i2p/syndie/ThreadNodeImpl.java new file mode 100644 index 000000000..2c42f688d --- /dev/null +++ b/apps/syndie/java/src/net/i2p/syndie/ThreadNodeImpl.java @@ -0,0 +1,104 @@ +package net.i2p.syndie; + +import java.util.*; +import net.i2p.data.Hash; +import net.i2p.syndie.data.BlogURI; +import net.i2p.syndie.data.ThreadNode; + +/** + * Simple memory intensive (but fast) node impl + * + */ +class ThreadNodeImpl implements ThreadNode { + /** write once, never updated once the tree is created */ + private Collection _recursiveAuthors; + /** contains the BlogURI instances */ + private Collection _recursiveEntries; + /** write once, never updated once the tree is created */ + private List _children; + private BlogURI _entry; + private ThreadNode _parent; + private BlogURI _parentEntry; + private Collection _tags; + private Collection _recursiveTags; + private long _mostRecentPostDate; + private Hash _mostRecentPostAuthor; + + public ThreadNodeImpl() { + _recursiveAuthors = new HashSet(1); + _recursiveEntries = new HashSet(1); + _children = new ArrayList(1); + _entry = null; + _parent = null; + _parentEntry = null; + _tags = new HashSet(); + _recursiveTags = new HashSet(); + _mostRecentPostDate = -1; + _mostRecentPostAuthor = null; + } + + void setEntry(BlogURI entry) { _entry = entry; } + void addAuthor(Hash author) { _recursiveAuthors.add(author); } + void addChild(ThreadNodeImpl child) { + if (!_children.contains(child)) + _children.add(child); + } + void setParent(ThreadNodeImpl parent) { _parent = parent; } + void setParentEntry(BlogURI parent) { _parentEntry = parent; } + void addTag(String tag) { + _tags.add(tag); + _recursiveTags.add(tag); + } + + void summarizeThread() { + _recursiveAuthors.add(_entry.getKeyHash()); + _recursiveEntries.add(_entry); + _mostRecentPostDate = _entry.getEntryId(); + _mostRecentPostAuthor = _entry.getKeyHash(); + + // we need to go through all children (recursively), in case the + // tree is out of order (which it shouldn't be, if its built carefully...) + for (int i = 0; i < _children.size(); i++) { + ThreadNodeImpl node = (ThreadNodeImpl)_children.get(i); + node.summarizeThread(); + if (node.getMostRecentPostDate() > _mostRecentPostDate) { + _mostRecentPostDate = node.getMostRecentPostDate(); + _mostRecentPostAuthor = node.getMostRecentPostAuthor(); + } + _recursiveTags.addAll(node.getRecursiveTags()); + _recursiveAuthors.addAll(node.getRecursiveAuthors()); + _recursiveEntries.addAll(node.getRecursiveEntries()); + } + } + + public String toString() { + StringBuffer buf = new StringBuffer(); + buf.append("").append(getEntry().toString()).append("\n"); + buf.append("").append(getTags()).append("\n"); + buf.append("").append(getRecursiveTags()).append("\n"); + buf.append("\n"); + for (int i = 0; i < _children.size(); i++) + buf.append(_children.get(i).toString()); + buf.append("\n"); + buf.append("\n"); + return buf.toString(); + } + + private Collection getRecursiveAuthors() { return _recursiveAuthors; } + private Collection getRecursiveEntries() { return _recursiveEntries; } + + // interface-specified methods doing what one would expect... + public boolean containsAuthor(Hash author) { return _recursiveAuthors.contains(author); } + public boolean containsEntry(BlogURI uri) { return _recursiveEntries.contains(uri); } + public ThreadNode getChild(int index) { return (ThreadNode)_children.get(index); } + public int getChildCount() { return _children.size(); } + public BlogURI getEntry() { return _entry; } + public ThreadNode getParent() { return _parent; } + public BlogURI getParentEntry() { return _parentEntry; } + public boolean containsTag(String tag) { return _tags.contains(tag); } + public Collection getTags() { return _tags; } + public Collection getRecursiveTags() { return _recursiveTags; } + public long getMostRecentPostDate() { return _mostRecentPostDate; } + public Hash getMostRecentPostAuthor() { return _mostRecentPostAuthor; } + public Iterator getRecursiveAuthorIterator() { return _recursiveAuthors.iterator(); } +} diff --git a/apps/syndie/java/src/net/i2p/syndie/WritableThreadIndex.java b/apps/syndie/java/src/net/i2p/syndie/WritableThreadIndex.java new file mode 100644 index 000000000..72c5f5b5c --- /dev/null +++ b/apps/syndie/java/src/net/i2p/syndie/WritableThreadIndex.java @@ -0,0 +1,148 @@ +package net.i2p.syndie; + +import java.util.*; +import net.i2p.I2PAppContext; +import net.i2p.data.DataHelper; +import net.i2p.data.Hash; +import net.i2p.syndie.data.*; +import net.i2p.syndie.sml.SMLParser; +import net.i2p.syndie.sml.HTMLRenderer; + +/** + * + */ +class WritableThreadIndex extends ThreadIndex { + /** map of child (BlogURI) to parent (BlogURI) */ + private Map _parents; + /** map of entry (BlogURI) to tags (String[]) */ + private Map _tags; + private static final String[] NO_TAGS = new String[0]; + /** b0rk if the thread seems to go too deep */ + private static final int MAX_THREAD_DEPTH = 64; + + WritableThreadIndex() { + super(); + _parents = new HashMap(); + _tags = new TreeMap(new NewestEntryFirstComparator()); + } + + void addParent(BlogURI parent, BlogURI child) { _parents.put(child, parent); } + void addEntry(BlogURI entry, String tags[]) { + if (tags == null) tags = NO_TAGS; + String oldTags[] = (String[])_tags.put(entry, tags); + } + + /** + * pull the data added together into threads, and stash them in the + * roots, organized chronologically + * + */ + void organizeTree() { + Map nodes = new HashMap(_tags.size()); + for (Iterator iter = _tags.keySet().iterator(); iter.hasNext(); ) { + BlogURI entry = (BlogURI)iter.next(); + String tags[] = (String[])_tags.get(entry); + BlogURI parent = (BlogURI)_parents.get(entry); + ThreadNodeImpl node = new ThreadNodeImpl(); + node.setEntry(entry); + if (tags != null) + for (int i = 0; i < tags.length; i++) + node.addTag(tags[i]); + if (parent != null) + node.setParentEntry(parent); + addEntry(entry, node); + nodes.put(entry, node); + } + + SMLParser parser = new SMLParser(I2PAppContext.getGlobalContext()); + HeaderReceiver rec = new HeaderReceiver(); + Archive archive = BlogManager.instance().getArchive(); + + TreeSet roots = new TreeSet(new NewestNodeFirstComparator()); + for (Iterator iter = nodes.keySet().iterator(); iter.hasNext(); ) { + BlogURI entry = (BlogURI)iter.next(); + ThreadNodeImpl node = (ThreadNodeImpl)nodes.get(entry); + int depth = 0; + // climb the tree + while (node.getParentEntry() != null) { + ThreadNodeImpl parent = (ThreadNodeImpl)nodes.get(node.getParentEntry()); + if (parent == null) break; + + // if the parent doesn't want replies, only include replies under the tree + // if they're written by the same author + BlogURI parentURI = parent.getEntry(); + EntryContainer parentEntry = archive.getEntry(parentURI); + if (parentEntry != null) { + parser.parse(parentEntry.getEntry().getText(), rec); + String refuse = rec.getHeader(HTMLRenderer.HEADER_REFUSE_REPLIES); + if ( (refuse != null) && (Boolean.valueOf(refuse).booleanValue()) ) { + if (parent.getEntry().getKeyHash().equals(entry.getKeyHash())) { + // same author, allow the reply + } else { + // different author, refuse + parent = null; + break; + } + } + } + + node.setParent(parent); + parent.addChild(node); + node = parent; + depth++; + if (depth > MAX_THREAD_DEPTH) + break; + } + + node.summarizeThread(); + roots.add(node); + } + + // store them, sorted by most recently updated thread first + for (Iterator iter = roots.iterator(); iter.hasNext(); ) + addRoot((ThreadNode)iter.next()); + + _parents.clear(); + _tags.clear(); + } + + public String toString() { + StringBuffer buf = new StringBuffer(); + buf.append(""); + for (int i = 0; i < getRootCount(); i++) { + ThreadNode root = getRoot(i); + buf.append(root.toString()); + } + buf.append("\n"); + return buf.toString(); + } + + /** sort BlogURI instances with the highest entryId first */ + private class NewestEntryFirstComparator implements Comparator { + public int compare(Object lhs, Object rhs) { + BlogURI left = (BlogURI)lhs; + BlogURI right = (BlogURI)rhs; + if (left.getEntryId() > right.getEntryId()) { + return -1; + } else if (left.getEntryId() == right.getEntryId()) { + return DataHelper.compareTo(left.getKeyHash().getData(), right.getKeyHash().getData()); + } else { + return 1; + } + } + } + /** sort ThreadNodeImpl instances with the highest entryId first */ + private class NewestNodeFirstComparator implements Comparator { + public int compare(Object lhs, Object rhs) { + ThreadNodeImpl left = (ThreadNodeImpl)lhs; + ThreadNodeImpl right = (ThreadNodeImpl)rhs; + if (left.getEntry().getEntryId() > right.getEntry().getEntryId()) { + return -1; + } else if (left.getEntry().getEntryId() == right.getEntry().getEntryId()) { + return DataHelper.compareTo(left.getEntry().getKeyHash().getData(), right.getEntry().getKeyHash().getData()); + } else { + return 1; + } + } + } +} diff --git a/apps/syndie/java/src/net/i2p/syndie/data/ArchiveIndex.java b/apps/syndie/java/src/net/i2p/syndie/data/ArchiveIndex.java index 09f5f6fcd..61126821a 100644 --- a/apps/syndie/java/src/net/i2p/syndie/data/ArchiveIndex.java +++ b/apps/syndie/java/src/net/i2p/syndie/data/ArchiveIndex.java @@ -32,6 +32,7 @@ public class ArchiveIndex { /** parent message to a set of replies, ordered with the oldest first */ protected Map _replies; protected Properties _headers; + private ThreadIndex _threadedIndex; public ArchiveIndex() { this(I2PAppContext.getGlobalContext(), false); @@ -48,6 +49,7 @@ public class ArchiveIndex { _headers = new Properties(); _replies = Collections.synchronizedMap(new HashMap()); _generatedOn = -1; + _threadedIndex = null; if (shouldLoad) setIsLocal("true"); } @@ -61,6 +63,8 @@ public class ArchiveIndex { public long getTotalSize() { return _totalSize; } public long getNewSize() { return _newSize; } public long getGeneratedOn() { return _generatedOn; } + public ThreadIndex getThreadedIndex() { return _threadedIndex; } + public void setThreadedIndex(ThreadIndex index) { _threadedIndex = index; } public String getNewSizeStr() { if (_newSize < 1024) return _newSize + ""; diff --git a/apps/syndie/java/src/net/i2p/syndie/data/BlogURI.java b/apps/syndie/java/src/net/i2p/syndie/data/BlogURI.java index 99d8b1707..5b38a55af 100644 --- a/apps/syndie/java/src/net/i2p/syndie/data/BlogURI.java +++ b/apps/syndie/java/src/net/i2p/syndie/data/BlogURI.java @@ -74,7 +74,9 @@ public class BlogURI { DataHelper.eq(_blogHash, ((BlogURI)obj)._blogHash); } public int hashCode() { - int rv = (int)_entryId; + int rv = (int)((_entryId >>> 32) & 0x7FFFFFFF); + rv += (_entryId & 0x7FFFFFFF); + if (_blogHash != null) rv += _blogHash.hashCode(); return rv; diff --git a/apps/syndie/java/src/net/i2p/syndie/data/FilteredThreadIndex.java b/apps/syndie/java/src/net/i2p/syndie/data/FilteredThreadIndex.java new file mode 100644 index 000000000..c5da4a485 --- /dev/null +++ b/apps/syndie/java/src/net/i2p/syndie/data/FilteredThreadIndex.java @@ -0,0 +1,88 @@ +package net.i2p.syndie.data; + +import java.util.*; +import net.i2p.syndie.*; +import net.i2p.data.*; +import net.i2p.client.naming.*; + +/** + * + */ +public class FilteredThreadIndex extends ThreadIndex { + private User _user; + private Archive _archive; + private ThreadIndex _baseIndex; + private Collection _filteredTags; + private List _roots; + private List _ignoredAuthors; + + public static final String GROUP_FAVORITE = "Favorite"; + public static final String GROUP_IGNORE = "Ignore"; + + public FilteredThreadIndex(User user, Archive archive, Collection tags) { + super(); + _user = user; + _archive = archive; + _baseIndex = _archive.getIndex().getThreadedIndex(); + _filteredTags = tags; + if (_filteredTags == null) + _filteredTags = Collections.EMPTY_SET; + + _ignoredAuthors = new ArrayList(); + for (Iterator iter = user.getPetNameDB().iterator(); iter.hasNext(); ) { + PetName pn = (PetName)iter.next(); + if (pn.isMember(GROUP_IGNORE)) { + try { + Hash h = new Hash(); + h.fromBase64(pn.getLocation()); + _ignoredAuthors.add(h); + } catch (DataFormatException dfe) { + // ignore + } + } + } + + filter(); + } + + private void filter() { + _roots = new ArrayList(_baseIndex.getRootCount()); + for (int i = 0; i < _baseIndex.getRootCount(); i++) { + ThreadNode node = _baseIndex.getRoot(i); + if (!isIgnored(node, _ignoredAuthors, _filteredTags)) + _roots.add(node); + } + } + + + private boolean isIgnored(ThreadNode node, List ignoredAuthors, Collection requestedTags) { + boolean allAuthorsIgnored = true; + for (Iterator iter = node.getRecursiveAuthorIterator(); iter.hasNext(); ) { + Hash author = (Hash)iter.next(); + if (!ignoredAuthors.contains(author)) { + allAuthorsIgnored = false; + break; + } + } + + if ( (allAuthorsIgnored) && (ignoredAuthors.size() > 0) ) + return true; + if (requestedTags.size() > 0) { + for (Iterator iter = requestedTags.iterator(); iter.hasNext(); ) + if (node.getRecursiveTags().contains(iter.next())) + return false; + // authors we aren't ignoring have posted in the thread, but the user is filtering + // posts by tags, and this thread doesn't include any of those tags + return true; + } else { + // we aren't filtering by tags, and we haven't been refused by the author + // filtering + return false; + } + } + + public int getRootCount() { return _roots.size(); } + public ThreadNode getRoot(int index) { return (ThreadNode)_roots.get(index); } + public ThreadNode getNode(BlogURI uri) { return _baseIndex.getNode(uri); } + public Collection getFilteredTags() { return _filteredTags; } +} diff --git a/apps/syndie/java/src/net/i2p/syndie/data/ThreadIndex.java b/apps/syndie/java/src/net/i2p/syndie/data/ThreadIndex.java new file mode 100644 index 000000000..3ba3b2833 --- /dev/null +++ b/apps/syndie/java/src/net/i2p/syndie/data/ThreadIndex.java @@ -0,0 +1,49 @@ +package net.i2p.syndie.data; + +import java.util.*; + +/** + * List of threads, ordered with the most recently updated thread first. + * Each node in the tree summarizes everything underneath it as well. + * + */ +public class ThreadIndex { + /** ordered list of threads, with most recent first */ + private List _roots; + /** map of BlogURI to ThreadNode */ + private Map _nodes; + + protected ThreadIndex() { + // no need to synchronize, since the thread index doesn't change after + // its first built + _roots = new ArrayList(); + _nodes = new HashMap(64); + } + + public int getRootCount() { return _roots.size(); } + public ThreadNode getRoot(int index) { return (ThreadNode)_roots.get(index); } + public ThreadNode getNode(BlogURI uri) { return (ThreadNode)_nodes.get(uri); } + /** + * get the root of the thread that the given uri is located in, or -1. + * The implementation depends only on getRoot/getNode/getRootCount and not on the + * data structures, so should be safe for subclasses who adjust those methods + * + */ + public int getRoot(BlogURI uri) { + ThreadNode node = getNode(uri); + if (node == null) return -1; + while (node.getParent() != null) + node = node.getParent(); + for (int i = 0; i < getRootCount(); i++) { + ThreadNode cur = getRoot(i); + if (cur.equals(node)) + return i; + } + return -1; + } + + /** call this in the right order - most recently updated thread first */ + protected void addRoot(ThreadNode node) { _roots.add(node); } + /** invocation order here doesn't matter */ + protected void addEntry(BlogURI uri, ThreadNode node) { _nodes.put(uri, node); } +} diff --git a/apps/syndie/java/src/net/i2p/syndie/data/ThreadNode.java b/apps/syndie/java/src/net/i2p/syndie/data/ThreadNode.java new file mode 100644 index 000000000..7ab3f8aa6 --- /dev/null +++ b/apps/syndie/java/src/net/i2p/syndie/data/ThreadNode.java @@ -0,0 +1,34 @@ +package net.i2p.syndie.data; + +import java.util.*; +import net.i2p.data.Hash; + +/** + * + */ +public interface ThreadNode { + /** current post */ + public BlogURI getEntry(); + /** how many direct replies there are to the current entry */ + public int getChildCount(); + /** the given direct reply */ + public ThreadNode getChild(int index); + /** parent this is actually a reply to */ + public BlogURI getParentEntry(); + /** parent in the tree, maybe not a direct parent, but the closest one */ + public ThreadNode getParent(); + /** true if this entry, or any child, is written by the given author */ + public boolean containsAuthor(Hash author); + /** true if this node, or any child, includes the given URI */ + public boolean containsEntry(BlogURI uri); + /** list of tags (String) of this node only */ + public Collection getTags(); + /** list of tags (String) of this node or any children in the tree */ + public Collection getRecursiveTags(); + /** date of the most recent post, recursive */ + public long getMostRecentPostDate(); + /** author of the most recent post, recurisve */ + public Hash getMostRecentPostAuthor(); + /** walk across the authors of the entire thread */ + public Iterator getRecursiveAuthorIterator(); +} diff --git a/apps/syndie/java/src/net/i2p/syndie/data/TransparentArchiveIndex.java b/apps/syndie/java/src/net/i2p/syndie/data/TransparentArchiveIndex.java index c2c39faee..49d9c45e7 100644 --- a/apps/syndie/java/src/net/i2p/syndie/data/TransparentArchiveIndex.java +++ b/apps/syndie/java/src/net/i2p/syndie/data/TransparentArchiveIndex.java @@ -25,6 +25,7 @@ public class TransparentArchiveIndex extends ArchiveIndex { public long getTotalSize() { return index().getTotalSize(); } public long getNewSize() { return index().getNewSize(); } public long getGeneratedOn() { return index().getGeneratedOn(); } + public ThreadIndex getThreadedIndex() { return index().getThreadedIndex(); } public String getNewSizeStr() { return index().getNewSizeStr(); } public String getTotalSizeStr() { return index().getTotalSizeStr(); } diff --git a/apps/syndie/java/src/net/i2p/syndie/sml/HTMLRenderer.java b/apps/syndie/java/src/net/i2p/syndie/sml/HTMLRenderer.java index 931ceacbe..d67231b07 100644 --- a/apps/syndie/java/src/net/i2p/syndie/sml/HTMLRenderer.java +++ b/apps/syndie/java/src/net/i2p/syndie/sml/HTMLRenderer.java @@ -755,6 +755,10 @@ public class HTMLRenderer extends EventReceiverImpl { public static final String HEADER_STYLE = "Style"; public static final String HEADER_PETNAME = "PetName"; public static final String HEADER_TAGS = "Tags"; + /** if set to true, don't display the message in the same thread, though keep a parent reference */ + public static final String HEADER_FORCE_NEW_THREAD = "ForceNewThread"; + /** if set to true, don't let anyone else reply in the same thread (but let the original author reply) */ + public static final String HEADER_REFUSE_REPLIES = "RefuseReplies"; private void renderSubjectCell() { _preBodyBuffer.append("
"); @@ -880,7 +884,7 @@ public class HTMLRenderer extends EventReceiverImpl { } private final SimpleDateFormat _dateFormat = new SimpleDateFormat("yyyy/MM/dd", Locale.UK); - private final String getEntryDate(long when) { + public final String getEntryDate(long when) { synchronized (_dateFormat) { try { String str = _dateFormat.format(new Date(when)); diff --git a/apps/syndie/java/src/net/i2p/syndie/web/ViewThreadedServlet.java b/apps/syndie/java/src/net/i2p/syndie/web/ViewThreadedServlet.java new file mode 100644 index 000000000..fcd5ebaa5 --- /dev/null +++ b/apps/syndie/java/src/net/i2p/syndie/web/ViewThreadedServlet.java @@ -0,0 +1,1053 @@ +package net.i2p.syndie.web; + +import java.io.*; +import java.util.*; + +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.ServletException; + +import net.i2p.I2PAppContext; +import net.i2p.client.naming.*; +import net.i2p.data.*; +import net.i2p.syndie.*; +import net.i2p.syndie.data.*; +import net.i2p.syndie.sml.*; + +/** + * + */ +public class ViewThreadedServlet extends HttpServlet { + /** what, if any, post should be rendered */ + public static final String PARAM_VIEW_POST = "post"; + /** what, if any, thread should be rendered in its entirety */ + public static final String PARAM_VIEW_THREAD = "thread"; + /** what post should be visible in the nav tree */ + public static final String PARAM_VISIBLE = "visible"; + public static final String PARAM_ADD_TO_GROUP_LOCATION = "addLocation"; + public static final String PARAM_ADD_TO_GROUP_NAME = "addGroup"; + /** index into the nav tree to start displaying */ + public static final String PARAM_OFFSET = "offset"; + public static final String PARAM_TAGS = "tags"; + + private static final boolean ALLOW_FILTER_BY_TAG = true; + + public void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + req.setCharacterEncoding("UTF-8"); + resp.setCharacterEncoding("UTF-8"); + resp.setContentType("text/html"); + + User user = (User)req.getSession().getAttribute("user"); + String login = req.getParameter("login"); + String pass = req.getParameter("password"); + String action = req.getParameter("action"); + boolean forceNewIndex = false; + + if (user == null) { + if ("Login".equals(action)) { + user = new User(); + BlogManager.instance().login(user, login, pass); // ignore failures - user will just be unauthorized + if (!user.getAuthenticated()) + user.invalidate(); + } else { + user = new User(); + BlogManager.instance().login(user, login, pass); // ignore failures - user will just be unauthorized + } + forceNewIndex = true; + } else if ("Login".equals(action)) { + user = new User(); + BlogManager.instance().login(user, login, pass); // ignore failures - user will just be unauthorized + if (!user.getAuthenticated()) + user.invalidate(); + forceNewIndex = true; + } + + req.getSession().setAttribute("user", user); + + if (user.getAuthenticated()) { + String loc = req.getParameter(PARAM_ADD_TO_GROUP_LOCATION); + String group = req.getParameter(PARAM_ADD_TO_GROUP_NAME); + if ( (loc != null) && (group != null) && (group.trim().length() > 0) ) { + try { + Hash key = new Hash(); + key.fromBase64(loc); + PetNameDB db = user.getPetNameDB(); + PetName pn = db.getByLocation(loc); + boolean isNew = false; + if (pn == null) { + isNew = true; + BlogInfo info = BlogManager.instance().getArchive().getBlogInfo(key); + String name = null; + if (info != null) + name = info.getProperty(BlogInfo.NAME); + else + name = loc.substring(0,6); + + if (db.containsName(name)) { + int i = 0; + while (db.containsName(name + i)) + i++; + name = name + i; + } + + pn = new PetName(name, "syndie", "syndieblog", loc); + } + pn.addGroup(group); + if (isNew) + db.add(pn); + BlogManager.instance().saveUser(user); + // if we are ignoring someone, we need to recalculate the filters + if (FilteredThreadIndex.GROUP_IGNORE.equals(group)) + forceNewIndex = true; + } catch (DataFormatException dfe) { + // bad loc, ignore + } + } + } + + FilteredThreadIndex index = (FilteredThreadIndex)req.getSession().getAttribute("threadIndex"); + + Collection tags = getFilteredTags(req); + if (forceNewIndex || (index == null) || (!index.getFilteredTags().equals(tags)) ) { + index = new FilteredThreadIndex(user, BlogManager.instance().getArchive(), getFilteredTags(req)); + req.getSession().setAttribute("threadIndex", index); + } + + render(user, req, resp.getWriter(), index); + } + + private void render(User user, HttpServletRequest req, PrintWriter out, ThreadIndex index) throws ServletException, IOException { + Archive archive = BlogManager.instance().getArchive(); + int numThreads = 10; + int threadOffset = getOffset(req); + if (threadOffset == -1) { + threadOffset = index.getRootCount() - numThreads; + } + if (threadOffset < 0) { + threadOffset = 0; + } + + BlogURI visibleEntry = getVisible(req); + + int offset = 0; + if ( empty(req, PARAM_OFFSET) && (visibleEntry != null) ) { + // we're on a permalink, so jump the tree to the given thread + threadOffset = index.getRoot(visibleEntry); + if (threadOffset < 0) + threadOffset = 0; + } + + renderBegin(user, req, out, index); + renderNavBar(user, req, out, index); + renderControlBar(user, req, out, index); + renderBody(user, req, out, index); + renderThreadNav(user, req, out, threadOffset, index); + renderThreadTree(user, req, out, threadOffset, visibleEntry, archive, index); + renderThreadNav(user, req, out, threadOffset, index); + renderEnd(user, req, out, index); + } + + private void renderBegin(User user, HttpServletRequest req, PrintWriter out, ThreadIndex index) throws IOException { + out.write(BEGIN_HTML); + } + private void renderNavBar(User user, HttpServletRequest req, PrintWriter out, ThreadIndex index) throws IOException { + //out.write("\n"); + out.write("\n"); + out.write("\n"); + if (user.getAuthenticated()) { + out.write("Logged in as "); + out.write(user.getUsername()); + out.write("\n"); + out.write("(switch)\n"); + out.write("Post a new thread\n"); + } else { + out.write("\n"); + out.write("Login: \n"); + out.write("Password: \n"); + out.write("\n"); + } + //out.write("\n"); + out.write("
\n"); + if (user.getAuthenticated() && user.getAllowAccessRemote()) { + out.write("Syndicate\n"); + out.write("Import RSS/Atom\n"); + out.write("Admin\n"); + } + out.write("\n\n"); + } + + private static final ArrayList SKIP_TAGS = new ArrayList(); + static { + SKIP_TAGS.add("action"); + SKIP_TAGS.add("filter"); + // post and visible are skipped since we aren't good at filtering by tag when the offset will + // skip around randomly. at least, not yet. + SKIP_TAGS.add("visible"); + //SKIP_TAGS.add("post"); + //SKIP_TAGS.add("thread"); + SKIP_TAGS.add("offset"); // if we are adjusting the filter, ignore the previous offset + SKIP_TAGS.add("login"); + SKIP_TAGS.add("password"); + } + + private void renderControlBar(User user, HttpServletRequest req, PrintWriter out, ThreadIndex index) throws IOException { + if (ALLOW_FILTER_BY_TAG) { + out.write("
\n"); + String tags = ""; + Enumeration params = req.getParameterNames(); + while (params.hasMoreElements()) { + String param = (String)params.nextElement(); + String val = req.getParameter(param); + if (PARAM_TAGS.equals(param)) { + tags = val; + } else if (SKIP_TAGS.contains(param)) { + // skip + } else if (param.length() <= 0) { + // skip + } else { + out.write("\n"); + } + } + out.write("\n"); + out.write("\n"); + out.write("Filter: \n"); + out.write("Tags: \n"); + out.write("\n"); + out.write("Threads\n"); + out.write("\n"); + out.write("\n"); + out.write("
\n"); + } else { + out.write(CONTROL_BAR_WITHOUT_TAGS); + } + } + private void renderBody(User user, HttpServletRequest req, PrintWriter out, ThreadIndex index) throws IOException, ServletException { + Archive archive = BlogManager.instance().getArchive(); + List posts = getPosts(archive, req, index); + for (int i = 0; i < posts.size(); i++) { + BlogURI post = (BlogURI)posts.get(i); + renderBody(user, req, out, archive, post, posts.size() == 1, index); + } + } + + private List getPosts(Archive archive, HttpServletRequest req, ThreadIndex index) { + List rv = new ArrayList(1); + String post = req.getParameter(PARAM_VIEW_POST); + BlogURI uri = getAsBlogURI(post); + if ( (uri != null) && (uri.getEntryId() > 0) ) { + rv.add(uri); + } else { + String thread = req.getParameter(PARAM_VIEW_THREAD); + uri = getAsBlogURI(thread); + if ( (uri != null) && (uri.getEntryId() > 0) ) { + ThreadNode node = index.getNode(uri); + if (node != null) { + while (node.getParent() != null) + node = node.getParent(); // hope the structure is loopless... + // depth first traversal + walkTree(rv, node); + } else { + rv.add(uri); + } + } + } + return rv; + } + + private void walkTree(List uris, ThreadNode node) { + if (node == null) + return; + if (uris.contains(node)) + return; + uris.add(node.getEntry()); + for (int i = 0; i < node.getChildCount(); i++) + walkTree(uris, node.getChild(i)); + } + + private void renderBody(User user, HttpServletRequest req, PrintWriter out, Archive archive, BlogURI post, boolean inlineReply, ThreadIndex index) throws IOException, ServletException { + EntryContainer entry = archive.getEntry(post); + if (entry == null) return; + + out.write("\n"); + out.write("\n"); + out.write("\n"); + + HeaderReceiver rec = new HeaderReceiver(); + SMLParser parser = new SMLParser(I2PAppContext.getGlobalContext()); + HTMLRenderer rend = new HTMLRenderer(I2PAppContext.getGlobalContext()); + parser.parse(entry.getEntry().getText(), rec); + String subject = rec.getHeader(HTMLRenderer.HEADER_SUBJECT); + if (subject == null) + subject = ""; + out.write(" "); + out.write(subject); + out.write("\n"); + out.write("\n"); + out.write(""); + + String author = null; + PetName pn = user.getPetNameDB().getByLocation(post.getKeyHash().toBase64()); + if (pn == null) { + BlogInfo info = archive.getBlogInfo(post.getKeyHash()); + if (info != null) + author = info.getProperty(BlogInfo.NAME); + } else { + author = pn.getName(); + } + if ( (author == null) || (author.trim().length() <= 0) ) + author = post.getKeyHash().toBase64().substring(0,6); + + ThreadNode node = index.getNode(post); + + out.write(author); + out.write(" @ "); + out.write(rend.getEntryDate(post.getEntryId())); + + Collection tags = node.getTags(); + if ( (tags != null) && (tags.size() > 0) ) { + out.write("\nTags: \n"); + for (Iterator tagIter = tags.iterator(); tagIter.hasNext(); ) { + String tag = (String)tagIter.next(); + if (ALLOW_FILTER_BY_TAG) { + out.write(""); + } + out.write(" " + tag); + if (ALLOW_FILTER_BY_TAG) + out.write("\n"); + } + } + + out.write("\npermalink\n"); + + out.write("\n\n"); + out.write("\n"); + out.write("\n"); + out.write("\n"); + out.write("\n"); + rend.render(user, archive, entry, out, false, true); + out.write("\n\n"); + out.write("\n"); + out.write("\n"); +/* +"\n" + +"
\n" + +" \n" + +" External links:\n" + +" http://foo.i2p/\n" + +" http://bar.i2p/\n" + +"
\n" + +" Attachments: \n" + +"
Full thread\n" + +" Prev in thread \n" + +" Next in thread \n" + +" \n" + +"
\n" + +"\n" + + */ + out.write("\n"); + if (inlineReply && user.getAuthenticated() ) { + String refuseReplies = rec.getHeader(HTMLRenderer.HEADER_REFUSE_REPLIES); + // show the reply form if we are the author or replies have not been explicitly rejected + if ( (user.getBlog().equals(post.getKeyHash())) || + (refuseReplies == null) || (!Boolean.valueOf(refuseReplies).booleanValue()) ) { + out.write("\n"); + out.write("
\n"); + out.write(""); + out.write(""); + out.write("\n"); + out.write("Reply: (SML reference)\n\n"); + out.write("\n"); + out.write("\n"); + out.write("\n"); + out.write("\n"); + out.write(" \n"); + out.write(" \n"); + out.write(" Tags: \n"); + out.write(" in a new thread? \n"); + out.write(" allow replies? \n"); + out.write(" attachment: \n"); + out.write(" \n\n
\n"); + out.write("\n"); + } + } + out.write("\n"); + } + private void renderThreadNav(User user, HttpServletRequest req, PrintWriter out, int threadOffset, ThreadIndex index) throws IOException { + out.write("\n"); + out.write("\n"); + out.write("<< First Page "); + if (threadOffset > 0) { + out.write("< Prev Page\n"); + } else { + out.write("< Prev Page\n"); + } + out.write("\n"); + + int max = index.getRootCount(); + if (threadOffset + 10 > max) { + out.write("Next Page> Last Page>>\n"); + } else { + out.write("Next Page> Last Page>>\n"); + } + out.write("\n"); + out.write("\n"); + } + + private void renderThreadTree(User user, HttpServletRequest req, PrintWriter out, int threadOffset, BlogURI visibleEntry, Archive archive, ThreadIndex index) throws IOException { + int numThreads = 10; + renderThreadTree(user, out, index, archive, req, threadOffset, numThreads, visibleEntry); + } + + private static final int getOffset(HttpServletRequest req) { + String off = req.getParameter(PARAM_OFFSET); + try { + return Integer.parseInt(off); + } catch (NumberFormatException nfe) { + return 0; + } + } + private static final BlogURI getVisible(HttpServletRequest req) { + return getAsBlogURI(req.getParameter(PARAM_VISIBLE)); + } + private static final BlogURI getAsBlogURI(String uri) { + if (uri != null) { + int split = uri.indexOf('/'); + if ( (split <= 0) || (split + 1 >= uri.length()) ) + return null; + String blog = uri.substring(0, split); + String id = uri.substring(split+1); + try { + Hash hash = new Hash(); + hash.fromBase64(blog); + long msgId = Long.parseLong(id); + if (msgId > 0) + return new BlogURI(hash, msgId); + } catch (DataFormatException dfe) { + return null; + } catch (NumberFormatException nfe) { + return null; + } + } + return null; + } + + private Collection getFilteredTags(HttpServletRequest req) { + String tags = req.getParameter(PARAM_TAGS); + if (tags != null) { + StringTokenizer tok = new StringTokenizer(tags, "\n\t "); + ArrayList rv = new ArrayList(); + while (tok.hasMoreTokens()) { + String tag = tok.nextToken().trim(); + if (tag.length() > 0) + rv.add(tag); + } + return rv; + } else { + return Collections.EMPTY_LIST; + } + } + + private void renderThreadTree(User user, PrintWriter out, ThreadIndex index, Archive archive, HttpServletRequest req, + int threadOffset, int numThreads, BlogURI visibleEntry) { + + if ( (visibleEntry != null) && (empty(req, PARAM_OFFSET)) ) { + // we want to jump to a specific thread in the nav + threadOffset = index.getRoot(visibleEntry); + } + + out.write("\n"); + if (threadOffset + numThreads > index.getRootCount()) + numThreads = index.getRootCount() - threadOffset; + TreeRenderState state = new TreeRenderState(new ArrayList()); + + for (int curRoot = threadOffset; curRoot < numThreads + threadOffset; curRoot++) { + ThreadNode node = index.getRoot(curRoot); + out.write("\n"); + renderThread(user, out, index, archive, req, node, 0, visibleEntry, state); + out.write("\n"); + } + out.write("\n"); + } + + /* + private void renderThreadTree(User user, PrintWriter out, ThreadIndex index, Archive archive, HttpServletRequest req, + int threadOffset, int numThreads, BlogURI visibleEntry) { + + List ignored = new ArrayList(); + for (Iterator iter = user.getPetNameDB().iterator(); iter.hasNext(); ) { + PetName pn = (PetName)iter.next(); + if (pn.isMember(GROUP_IGNORE)) { + ignored.add(new Hash(Base64.decode(pn.getLocation()))); + } + } + + out.write("\n"); + if (threadOffset + numThreads > index.getRootCount()) + numThreads = index.getRootCount() - threadOffset; + TreeRenderState state = new TreeRenderState(ignored); + + Collection requestedTags = getFilteredTags(req); + out.write("\n"); + + int writtenThreads = 0; + int skipped = 0; + for (int curRoot = 0; (curRoot < index.getRootCount()) && (writtenThreads < numThreads); curRoot++) { + ThreadNode node = index.getRoot(curRoot); + boolean isIgnored = isIgnored(node, ignored, requestedTags); + out.write("\n"); + if (!isIgnored) { + if ( (writtenThreads + skipped >= threadOffset) || ( (visibleEntry != null) && (empty(req, PARAM_OFFSET)) ) ) { + renderThread(user, out, index, archive, req, node, 0, visibleEntry, state, requestedTags); + writtenThreads++; + } else { + skipped++; + } + } + out.write("\n"); + } + out.write("\n"); + } + */ + /** + * @return true if some post in the thread has been written + */ + private boolean renderThread(User user, PrintWriter out, ThreadIndex index, Archive archive, HttpServletRequest req, + ThreadNode node, int depth, BlogURI visibleEntry, TreeRenderState state) { + boolean isFavorite = false; + + HTMLRenderer rend = new HTMLRenderer(I2PAppContext.getGlobalContext()); + SMLParser parser = new SMLParser(I2PAppContext.getGlobalContext()); + + PetName pn = user.getPetNameDB().getByLocation(node.getEntry().getKeyHash().toBase64()); + if (pn != null) { + if (pn.isMember(FilteredThreadIndex.GROUP_FAVORITE)) { + isFavorite = true; + } + } + + state.incrementRowsWritten(); + if (state.getRowsWritten() % 2 == 0) + out.write("\n"); + else + out.write("\n"); + + out.write(""); + out.write(getFlagHTML(user, node)); + out.write("\n\n"); + for (int i = 0; i < depth; i++) + out.write("\"\""); + + boolean showChildren = false; + + int childCount = node.getChildCount(); + /* + for (int i = 0; i < node.getChildCount(); i++) { + ThreadNode child = node.getChild(i); + // we don't actually filter with the tags here, since something in this thread has already + // picked it out for rendering, and we don't want to limit it to just the subthreads that are + // tagged + if (isIgnored(child, state.getIgnoredAuthors(), Collections.EMPTY_LIST)) + childCount--; + } + */ + + if (childCount > 0) { + boolean allowCollapse = false; + + if (visibleEntry != null) { + if (node.getEntry().equals(visibleEntry)) { + // noop + } else if (node.containsEntry(visibleEntry)) { + showChildren = true; + allowCollapse = true; + } + } else { + // noop + } + + if (allowCollapse) { + out.write("\"-\"\n"); + } else { + out.write("\"+\"\n"); + } + } else { + out.write("\"\"\n"); + } + + out.write(""); + + if (pn == null) { + BlogInfo info = archive.getBlogInfo(node.getEntry().getKeyHash()); + String name = null; + if (info != null) + name = info.getProperty(BlogInfo.NAME); + if ( (name == null) || (name.trim().length() <= 0) ) + name = node.getEntry().getKeyHash().toBase64().substring(0,6); + out.write(name); + } else { + out.write(pn.getName()); + } + out.write("\n"); + + if (isFavorite) { + out.write("\"favorites\"\n"); + } else { + if (user.getAuthenticated()) { + // give them a link to bookmark or ignore the peer + out.write("(\"friend\"\n"); + out.write("/\"ignore\")\n"); + } + } + + out.write(" @ "); + out.write(""); + out.write(rend.getEntryDate(node.getEntry().getEntryId())); + out.write(": "); + EntryContainer entry = archive.getEntry(node.getEntry()); + + HeaderReceiver rec = new HeaderReceiver(); + parser.parse(entry.getEntry().getText(), rec); + String subject = rec.getHeader(HTMLRenderer.HEADER_SUBJECT); + if (subject == null) + subject = ""; + out.write(subject); + out.write("\n\n"); + out.write("view thread\n"); + out.write("\n"); + + boolean rendered = true; + + if (showChildren) { + for (int i = 0; i < node.getChildCount(); i++) { + ThreadNode child = node.getChild(i); + boolean childRendered = renderThread(user, out, index, archive, req, child, depth+1, visibleEntry, state); + rendered = rendered || childRendered; + } + } + + return rendered; + } + + + private String getFlagHTML(User user, ThreadNode node) { + // grab all of the peers in the user's favorites group and check to see if + // they posted something in the given thread, flagging it if they have + boolean favoriteFound = false; + for (Iterator iter = user.getPetNameDB().getNames().iterator(); iter.hasNext(); ) { + PetName pn = user.getPetNameDB().getByName((String)iter.next()); + if (pn.isMember(FilteredThreadIndex.GROUP_FAVORITE)) { + Hash cur = new Hash(); + try { + cur.fromBase64(pn.getLocation()); + if (node.containsAuthor(cur)) { + favoriteFound = true; + break; + } + } catch (Exception e) {} + } + } + if (favoriteFound) + return "\"flagged"; + else + return " "; + } + + private static final boolean empty(HttpServletRequest req, String param) { + String val = req.getParameter(param); + return (val == null) || (val.trim().length() <= 0); + } + + private String getExpandLink(HttpServletRequest req, ThreadNode node) { + StringBuffer buf = new StringBuffer(64); + buf.append(req.getRequestURI()); + buf.append('?'); + // expand node == let one of node's children be visible + if (node.getChildCount() > 0) { + ThreadNode child = node.getChild(0); + buf.append(PARAM_VISIBLE).append('='); + buf.append(child.getEntry().getKeyHash().toBase64()).append('/'); + buf.append(child.getEntry().getEntryId()).append('&'); + } + + if (!empty(req, PARAM_VIEW_POST)) + buf.append(PARAM_VIEW_POST).append('=').append(req.getParameter(PARAM_VIEW_POST)).append('&'); + else if (!empty(req, PARAM_VIEW_THREAD)) + buf.append(PARAM_VIEW_THREAD).append('=').append(req.getParameter(PARAM_VIEW_THREAD)).append('&'); + + if (!empty(req, PARAM_OFFSET)) + buf.append(PARAM_OFFSET).append('=').append(req.getParameter(PARAM_OFFSET)).append('&'); + + if (!empty(req, PARAM_TAGS)) + buf.append(PARAM_TAGS).append('=').append(req.getParameter(PARAM_TAGS)).append('&'); + + return buf.toString(); + } + private String getCollapseLink(HttpServletRequest req, ThreadNode node) { + StringBuffer buf = new StringBuffer(64); + buf.append(req.getRequestURI()); + // collapse node == let the node be visible + buf.append('?').append(PARAM_VISIBLE).append('='); + buf.append(node.getEntry().getKeyHash().toBase64()).append('/'); + buf.append(node.getEntry().getEntryId()).append('&'); + + if (!empty(req, PARAM_VIEW_POST)) + buf.append(PARAM_VIEW_POST).append('=').append(req.getParameter(PARAM_VIEW_POST)).append('&'); + else if (!empty(req, PARAM_VIEW_THREAD)) + buf.append(PARAM_VIEW_THREAD).append('=').append(req.getParameter(PARAM_VIEW_THREAD)).append('&'); + + if (!empty(req, PARAM_OFFSET)) + buf.append(PARAM_OFFSET).append('=').append(req.getParameter(PARAM_OFFSET)).append('&'); + + if (!empty(req, PARAM_TAGS)) + buf.append(PARAM_TAGS).append('=').append(req.getParameter(PARAM_TAGS)).append('&'); + + return buf.toString(); + } + private String getProfileLink(HttpServletRequest req, Hash author) { + return HTMLRenderer.getMetadataURL(author); + } + private String getAddToGroupLink(HttpServletRequest req, Hash author, User user, String group) { + StringBuffer buf = new StringBuffer(64); + buf.append(req.getRequestURI()); + buf.append('?'); + String visible = req.getParameter(PARAM_VISIBLE); + if (visible != null) + buf.append(PARAM_VISIBLE).append('=').append(visible).append('&'); + buf.append(PARAM_ADD_TO_GROUP_LOCATION).append('=').append(author.toBase64()).append('&'); + buf.append(PARAM_ADD_TO_GROUP_NAME).append('=').append(group).append('&'); + + if (!empty(req, PARAM_VIEW_POST)) + buf.append(PARAM_VIEW_POST).append('=').append(req.getParameter(PARAM_VIEW_POST)).append('&'); + else if (!empty(req, PARAM_VIEW_THREAD)) + buf.append(PARAM_VIEW_THREAD).append('=').append(req.getParameter(PARAM_VIEW_THREAD)).append('&'); + + if (!empty(req, PARAM_OFFSET)) + buf.append(PARAM_OFFSET).append('=').append(req.getParameter(PARAM_OFFSET)).append('&'); + + if (!empty(req, PARAM_TAGS)) + buf.append(PARAM_TAGS).append('=').append(req.getParameter(PARAM_TAGS)).append('&'); + + return buf.toString(); + } + private String getViewPostLink(HttpServletRequest req, ThreadNode node, User user, boolean isPermalink) { + StringBuffer buf = new StringBuffer(64); + buf.append(req.getRequestURI()); + if (node.getChildCount() > 0) { + buf.append('?').append(PARAM_VISIBLE).append('='); + ThreadNode child = node.getChild(0); + buf.append(child.getEntry().getKeyHash().toBase64()).append('/'); + buf.append(child.getEntry().getEntryId()).append('&'); + } else { + buf.append('?').append(PARAM_VISIBLE).append('='); + buf.append(node.getEntry().getKeyHash().toBase64()).append('/'); + buf.append(node.getEntry().getEntryId()).append('&'); + } + buf.append(PARAM_VIEW_POST).append('='); + buf.append(node.getEntry().getKeyHash().toBase64()).append('/'); + buf.append(node.getEntry().getEntryId()).append('&'); + + if ( (!isPermalink) && (!empty(req, PARAM_OFFSET)) ) + buf.append(PARAM_OFFSET).append('=').append(req.getParameter(PARAM_OFFSET)).append('&'); + + if ( (!isPermalink) && (!empty(req, PARAM_TAGS)) ) + buf.append(PARAM_TAGS).append('=').append(req.getParameter(PARAM_TAGS)).append('&'); + + return buf.toString(); + } + private String getViewThreadLink(HttpServletRequest req, ThreadNode node, User user) { + StringBuffer buf = new StringBuffer(64); + buf.append(req.getRequestURI()); + if (node.getChildCount() > 0) { + buf.append('?').append(PARAM_VISIBLE).append('='); + ThreadNode child = node.getChild(0); + buf.append(child.getEntry().getKeyHash().toBase64()).append('/'); + buf.append(child.getEntry().getEntryId()).append('&'); + } else { + buf.append('?').append(PARAM_VISIBLE).append('='); + buf.append(node.getEntry().getKeyHash().toBase64()).append('/'); + buf.append(node.getEntry().getEntryId()).append('&'); + } + buf.append(PARAM_VIEW_THREAD).append('='); + buf.append(node.getEntry().getKeyHash().toBase64()).append('/'); + buf.append(node.getEntry().getEntryId()).append('&'); + + if (!empty(req, PARAM_OFFSET)) + buf.append(PARAM_OFFSET).append('=').append(req.getParameter(PARAM_OFFSET)).append('&'); + + if (!empty(req, PARAM_TAGS)) + buf.append(PARAM_TAGS).append('=').append(req.getParameter(PARAM_TAGS)).append('&'); + + buf.append("#").append(node.getEntry().toString()); + return buf.toString(); + } + private String getFilterByTagLink(HttpServletRequest req, ThreadNode node, User user, String tag) { + StringBuffer buf = new StringBuffer(64); + buf.append(req.getRequestURI()).append('?'); + /* + if (node.getChildCount() > 0) { + buf.append('?').append(PARAM_VISIBLE).append('='); + ThreadNode child = node.getChild(0); + buf.append(child.getEntry().getKeyHash().toBase64()).append('/'); + buf.append(child.getEntry().getEntryId()).append('&'); + } else { + buf.append('?').append(PARAM_VISIBLE).append('='); + buf.append(node.getEntry().getKeyHash().toBase64()).append('/'); + buf.append(node.getEntry().getEntryId()).append('&'); + } + */ + if (node != null) { + buf.append(PARAM_VIEW_POST).append('='); + buf.append(node.getEntry().getKeyHash().toBase64()).append('/'); + buf.append(node.getEntry().getEntryId()).append('&'); + } + + //if (!empty(req, PARAM_OFFSET)) + // buf.append(PARAM_OFFSET).append('=').append(req.getParameter(PARAM_OFFSET)).append('&'); + + if ( (tag != null) && (tag.trim().length() > 0) ) + buf.append(PARAM_TAGS).append('=').append(tag); + return buf.toString(); + } + private String getNavLink(HttpServletRequest req, int offset) { + StringBuffer buf = new StringBuffer(64); + buf.append(req.getRequestURI()); + buf.append('?'); + if (!empty(req, PARAM_VIEW_POST)) + buf.append(PARAM_VIEW_POST).append('=').append(req.getParameter(PARAM_VIEW_POST)).append('&'); + else if (!empty(req, PARAM_VIEW_THREAD)) + buf.append(PARAM_VIEW_THREAD).append('=').append(req.getParameter(PARAM_VIEW_THREAD)).append('&'); + + if (!empty(req, PARAM_TAGS)) + buf.append(PARAM_TAGS).append('=').append(req.getParameter(PARAM_TAGS)).append('&'); + + buf.append(PARAM_OFFSET).append('=').append(offset).append('&'); + + return buf.toString(); + } + + + private void renderEnd(User user, HttpServletRequest req, PrintWriter out, ThreadIndex index) throws IOException { + out.write(END_HTML); + } + + private static final String BEGIN_HTML = "\n" + +"\n" + +"Syndie\n" + +"\n" + +"\n" + +"\n" + +"\n"; + + private static final String CONTROL_BAR_WITHOUT_TAGS = "\n" + +"\n" + +"\n" + +"\n" + +"\n"; + + private static final String BODY = "\n" + +"\n" + +"\n" + +" \n" + +" \n" + +"\n" + +"\n" + +"\n" + +"\n" + +" \n" + +"\n" + +"\n" + +"\n" + +"\n" + +" \n" + +" \n" + +" \n" + +"\n" + +"\n" + +"\n" + +"\n" + +"\n" + +" \n" + +"\n" + +"\n" + +" \n" + +"\n" + +"\n" + +" \n" + +"\n" + +"\n" + +"\n" + +"\n"; + + + private static final String END_HTML = "
\n" + +"\n" + +"Filter: \n" + +"\n" + +"Threads
This is my subject\n" + +" petname @ 2005/11/08\n" + +" permalink\n" + +"
\n" + +" Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Vestibulum iaculis ante ac nisi. \n" + +" Ut ut justo sed sem venenatis elementum. Donec in erat. Duis felis erat, adipiscing eget, mattis\n" + +" sed, volutpat nec, lorem. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur \n" + +" ridiculus mus. Phasellus porta lacus ac metus. Suspendisse mi. Nulla facilisi. Phasellus metus. \n" + +" Nam varius elit ut magna. Suspendisse lectus massa, tempus vel, malesuada et, dictum quis, arcu. \n" + +" Ut auctor enim vel tellus.\n" + +"
\n" + +" External links:\n" + +" http://foo.i2p/\n" + +" http://bar.i2p/\n" + +"
\n" + +" Attachments: \n" + +"
Full thread\n" + +" Prev in thread \n" + +" Next in thread \n" + +"
Reply: (SML reference)
\n" + +" \n" + +" Tags: \n" + +" in a new thread? \n" + +" allow replies? \n" + +" attachment: \n" + +"
\n" + +"\n"; + + private static class TreeRenderState { + private int _rowsWritten; + private int _rowsSkipped; + private List _ignored; + public TreeRenderState(List ignored) { + _rowsWritten = 0; + _rowsSkipped = 0; + _ignored = ignored; + } + public int getRowsWritten() { return _rowsWritten; } + public void incrementRowsWritten() { _rowsWritten++; } + public int getRowsSkipped() { return _rowsSkipped; } + public void incrementRowsSkipped() { _rowsSkipped++; } + public List getIgnoredAuthors() { return _ignored; } + } + +} diff --git a/apps/syndie/jsp/images/addToFavorites.png b/apps/syndie/jsp/images/addToFavorites.png new file mode 100644 index 0000000000000000000000000000000000000000..95ded8d979a2c86a3a261636862795ab52fc2f3c GIT binary patch literal 275 zcmeAS@N?(olHy`uVBq!ia0vp^AT}2V8<6ZZI=>f4u_bxCyDx`7I;J! zGca%qgD@k*tT_@u!Ofm7jv*44dnf4fH3tZ^>_4tKE9Clj4i1rJD<4YCX*3oP;ar!n z)5-rsP;`KXfCW#UR@}nQXx%!c)r#4>L=AfVhj>4L=~oATm$vhQ}enqKkxb9bT5 zbG|MnzvWluYxx~FeR#6UlX4x&)eh@**5E$sodxM2j)8B2f|Zh SzK8&w&*16m=d#Wzp$PyOlxc_n literal 0 HcmV?d00001 diff --git a/apps/syndie/jsp/images/addToIgnored.png b/apps/syndie/jsp/images/addToIgnored.png new file mode 100644 index 0000000000000000000000000000000000000000..5b87bf45bc46333e5fffc775cf475e17620a9834 GIT binary patch literal 266 zcmeAS@N?(olHy`uVBq!ia0vp^AT}2V8<6ZZI=>f4u_bxCyDx`7I;J! zGca%qgD@k*tT_@u!Bw6vjv*44dnXujH9Ls7%%7)pL@!~!@|v}=QvDATb@sNn->^M6 z>E5YxhnPFgF8T4QO~A>H=l^D&&1X*EaDOFpM%PCETao;OOR1649?On-E>*D$@yfb0 z^UK{YdGBKK?)M(yaAh<+wCRQ|4}-G7c8{$pTXa^u&t1Rz?&Xpt*Q|Q?hiY#xGgEX3 zo$2%{i>1%#p7QgcuHqBbDtA*}l-b-{{_J+z?cc9E)qjis;5J%&P~6XEV++vL44$rj JF6*2UngBB|Yc>D? literal 0 HcmV?d00001 diff --git a/apps/syndie/jsp/images/collapse.png b/apps/syndie/jsp/images/collapse.png new file mode 100644 index 0000000000000000000000000000000000000000..2ce31b36d88ff5b3a23caaeb92cd9b3a5557fcce GIT binary patch literal 917 zcmc(eziO0G5XH|LWaBO=f{h9RdmCXMKqI?m7j+3Sja^WfS}=uatrQDwNAe75MN3#!S-AdbK#>xBt(!xA^}2>&`pr(%-{} z$4kk%kwQwTWRjW8Wg$yhb>2m;m4sBTi!gyeP+L)M6!mK$C9l zfD?jI2y4I~O((SA!CH1zTZtyVB50nOvUmM{-ZV-z?4;oSdyan7?pIX&7u*4>{!`c;@beQ>bQ z>&ts9f-BEgPhLp#tNoei!}&EXr>E;j%jviIboctL&C8E(x!PSX_K&x}KRdZOx&39b Yb;jHERr9@1@Bfh0dbwF#+&g*o4`Ex`jQ{`u literal 0 HcmV?d00001 diff --git a/apps/syndie/jsp/images/expand.png b/apps/syndie/jsp/images/expand.png new file mode 100644 index 0000000000000000000000000000000000000000..95c0c4c81a2db8c4d58842c15e1a0d08148fab9b GIT binary patch literal 922 zcmc(ey=s(U5QWbwvT;KSu}KycTMJQ`3-E_svWvQem_}?Cm1%4Q7wiK8#llXQ;5B#w zUc*>QY;CM81UsAYneQfiVSnbGnKS2{`M!4ttNoqHbRubIxtJaD+y5t9=lTBf>&^%1 z+~0$TM{~)gkWwm{WF~W2$Wm6Fbd$S0_9&x zPzYJK=IV`D34Zin! z2P>Q7;7)IQ5Zsgp4ZZwf4u_bxCyDx`7I;J! zGca%qgD@k*tT_@uLG}_)Usv|4+?>q3(mRU+;u#niV?13PLnJPT_MO*mE@U`n|2{*n z`efWT^UZsud5=!BD0ue7{ST{N*33CEcVsOD#dogTQ5f)#;b>&Evf;+^YV+!|72Q#X z?yWy8G_UH_=PSiOJa!!64B#k_E-G0d5olW_-Vn)mZ@GT0`1bO(`3}olyqB;qzIo-- zrFU~KXMQl3m#^DaCiXq(4nSzdKfZ{_Lb(!5vKZ~ZyxX062AdKp>ie5dQ7pIE1^%oI((%<)=U`}Xwkxpth> zD;V@l{%1SKub2F`O`5UcxO)A+Uv7KPN}e@U_{3ZDYnr6sf4u_bxCyDx`7I;J! zGca%qgD@k*tT_@uLG}_)Usv|4-0Xsq5+7Gwn+p`;^K@|xk+__kAi>%URLeMvfsuRB RTzQ}jgQu&X%Q~loCIIJ19ZvuN literal 0 HcmV?d00001 diff --git a/apps/syndie/jsp/images/threadIndent.png b/apps/syndie/jsp/images/threadIndent.png new file mode 100644 index 0000000000000000000000000000000000000000..3ea13d3a131c793fc3cabe91a9861d69767a2164 GIT binary patch literal 129 zcmeAS@N?(olHy`uVBq!ia0vp^AT}2V8<6ZZI=>f4u_bxCyDx`7I;J! zGca%qgD@k*tT_@uLG}_)Usv|4-0Xsq5+7Gwn+p`;^K@|xk+__kAi>%URLeMvfsuRB RTzQ}jgQu&X%Q~loCIIJ19ZvuN literal 0 HcmV?d00001 diff --git a/apps/syndie/jsp/index.html b/apps/syndie/jsp/index.html new file mode 100644 index 000000000..90c07f077 --- /dev/null +++ b/apps/syndie/jsp/index.html @@ -0,0 +1,3 @@ +SyndieEnter diff --git a/apps/syndie/jsp/switchuser.jsp b/apps/syndie/jsp/switchuser.jsp new file mode 100644 index 000000000..64a31219f --- /dev/null +++ b/apps/syndie/jsp/switchuser.jsp @@ -0,0 +1,16 @@ +<%@page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" import="net.i2p.syndie.web.*" %><% +request.setCharacterEncoding("UTF-8"); +%> + + +Syndie + + + +
+Syndie login:
+Password:
+ + +
+ \ No newline at end of file diff --git a/apps/syndie/jsp/web.xml b/apps/syndie/jsp/web.xml index 1f5798c83..acdb2e131 100644 --- a/apps/syndie/jsp/web.xml +++ b/apps/syndie/jsp/web.xml @@ -14,18 +14,25 @@ net.i2p.syndie.web.RSSServlet + + net.i2p.syndie.web.ViewThreadedServlet + net.i2p.syndie.web.ViewThreadedServlet + + net.i2p.syndie.UpdaterServlet net.i2p.syndie.UpdaterServlet - 1 - + 1 + + net.i2p.syndie.web.ArchiveServlet /archive/* @@ -34,6 +41,10 @@ net.i2p.syndie.web.RSSServlet /rss.jsp + + net.i2p.syndie.web.ViewThreadedServlet + /threads.jsp +