From f6979c811fa51c46907cd0220fc28242e68324fe Mon Sep 17 00:00:00 2001 From: jrandom <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 0487cabe1f..22232e3de0 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 a22b31436c..379a390d8e 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 861a248194..5189437c20 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 0000000000..9a9c2aa199 --- /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 0000000000..2c42f688d6 --- /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("<node><entry>").append(getEntry().toString()).append("</entry>\n"); + buf.append("<tags>").append(getTags()).append("</tags>\n"); + buf.append("<recursiveTags>").append(getRecursiveTags()).append("</recursiveTags>\n"); + buf.append("<children>\n"); + for (int i = 0; i < _children.size(); i++) + buf.append(_children.get(i).toString()); + buf.append("</children>\n"); + buf.append("</node>\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 0000000000..72c5f5b5ca --- /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("<threadIndex>"); + for (int i = 0; i < getRootCount(); i++) { + ThreadNode root = getRoot(i); + buf.append(root.toString()); + } + buf.append("</threadIndex>\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 09f5f6fcdf..61126821ae 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 99d8b17078..5b38a55af9 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 0000000000..c5da4a485c --- /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 0000000000..3ba3b2833b --- /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 0000000000..7ab3f8aa63 --- /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 c2c39faee2..49d9c45e7c 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 931ceacbe8..d67231b079 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("<form action=\"index.jsp\">"); @@ -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 0000000000..fcd5ebaa51 --- /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("<tr class=\"topNav\"><td class=\"topNav_user\" colspan=\"2\" nowrap=\"true\">\n"); + out.write("<tr class=\"topNav\"><td colspan=\"3\" nowrap=\"true\"><span class=\"topNav_user\">\n"); + out.write("<!-- nav bar begin -->\n"); + if (user.getAuthenticated()) { + out.write("Logged in as <a href=\"" + getProfileLink(req, user.getBlog()) + "\" title=\"Edit your profile\">"); + out.write(user.getUsername()); + out.write("</a>\n"); + out.write("(<a href=\"switchuser.jsp\" title=\"Log in as another user\">switch</a>)\n"); + out.write("<a href=\"post.jsp\" title=\"Post a new thread\">Post a new thread</a>\n"); + } else { + out.write("<form action=\"" + req.getRequestURI() + "\" method=\"GET\">\n"); + out.write("Login: <input type=\"text\" name=\"login\" />\n"); + out.write("Password: <input type=\"password\" name=\"password\" />\n"); + out.write("<input type=\"submit\" name=\"action\" value=\"Login\" /></form>\n"); + } + //out.write("</td><td class=\"topNav_admin\">\n"); + out.write("</span><span class=\"topNav_admin\">\n"); + if (user.getAuthenticated() && user.getAllowAccessRemote()) { + out.write("<a href=\"syndicate.jsp\" title=\"Syndicate data between other Syndie nodes\">Syndicate</a>\n"); + out.write("<a href=\"importfeed.jsp\" title=\"Import RSS/Atom data\">Import RSS/Atom</a>\n"); + out.write("<a href=\"admin.jsp\" title=\"Configure this Syndie node\">Admin</a>\n"); + } + out.write("</span><!-- nav bar end -->\n</td></tr>\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("<form action=\""); + out.write(req.getRequestURI()); + out.write("\" method=\"GET\">\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("<input type=\"hidden\" name=\"" + param + "\" value=\"" + val + "\" />\n"); + } + } + out.write("<tr class=\"controlBar\"><td colspan=\"2\">\n"); + out.write("<!-- control bar begin -->\n"); + out.write("Filter: <select name=\"filter\" disabled=\"true\" >\n"); + out.write(" <option value=\"all\">All posts in all threads</option>\n"); + out.write(" <option value=\"self\">Threads you have posted in</option>\n"); + out.write(" <option value=\"favorites\">Threads your friends have posted in</option>\n"); + out.write(" </select>\n"); + out.write("Tags: <input type=\"text\" name=\"" + PARAM_TAGS + "\" size=\"30\" value=\"" + tags + "\" />\n"); + out.write("<input type=\"submit\" name=\"action\" value=\"Go\" />\n"); + out.write("</td><td class=\"controlBarRight\"><a href=\"#threads\" title=\"Jump to the thread navigation\">Threads</a></td>\n"); + out.write("<!-- control bar end -->\n"); + out.write("</tr>\n"); + out.write("</form>\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("<!-- body begin -->\n"); + out.write("<!-- body meta begin -->\n"); + out.write("<tr class=\"postMeta\" id=\"" + post.toString() + "\">\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(" <td colspan=\"3\" class=\"postMetaSubject\">"); + out.write(subject); + out.write("</td></tr>\n"); + out.write("<tr class=\"postMeta\"><td colspan=\"3\" class=\"postMetaLink\">\n"); + out.write("<a href=\""); + out.write(HTMLRenderer.getMetadataURL(post.getKeyHash())); + out.write("\" title=\"View the author's profile\">"); + + 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("</a> @ "); + 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("<a href=\""); + out.write(getFilterByTagLink(req, node, user, tag)); + out.write("\" title=\"Filter threads to only include posts tagged as '"); + out.write(tag); + out.write("'\">"); + } + out.write(" " + tag); + if (ALLOW_FILTER_BY_TAG) + out.write("</a>\n"); + } + } + + out.write("\n<a href=\""); + out.write(getViewPostLink(req, node, user, true)); + out.write("\" title=\"Select a shareable link directly to this post\">permalink</a>\n"); + + out.write("</td>\n</tr>\n"); + out.write("<!-- body meta end -->\n"); + out.write("<!-- body post begin -->\n"); + out.write("<tr class=\"postData\">\n"); + out.write("<td colspan=\"3\">\n"); + rend.render(user, archive, entry, out, false, true); + out.write("</td>\n</tr>\n"); + out.write("<!-- body post end -->\n"); + out.write("<!-- body details begin -->\n"); +/* +"<tr class=\"postDetails\">\n" + +" <form action=\"viewattachment.jsp\" method=\"GET\">\n" + +" <td colspan=\"3\">\n" + +" External links:\n" + +" <a href=\"external.jsp?foo\" title=\"View foo.i2p\">http://foo.i2p/</a>\n" + +" <a href=\"external.jsp?bar\" title=\"View bar.i2p\">http://bar.i2p/</a>\n" + +" <br />\n" + +" Attachments: <select name=\"attachment\">\n" + +" <option value=\"0\">sampleRawSML.sml: Sample SML file with headers (4KB, type text/plain)</option>\n" + +" </select> <input type=\"submit\" name=\"action\" value=\"Download\" />\n" + +" <br /><a href=\"\" title=\"Expand the entire thread\">Full thread</a>\n" + +" <a href=\"\" title=\"Previous post in the thread\">Prev in thread</a> \n" + +" <a href=\"\" title=\"Next post in the thread\">Next in thread</a> \n" + +" </td>\n" + +" </form>\n" + +"</tr>\n" + + */ + out.write("<!-- body details end -->\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("<!-- body reply begin -->\n"); + out.write("<form action=\"post.jsp\" method=\"POST\" enctype=\"multipart/form-data\">\n"); + out.write("<input type=\"hidden\" name=\"inReplyTo\" value=\""); + out.write(Base64.encode(post.toString())); + out.write("\" />"); + out.write("<input type=\"hidden\" name=\"entrysubject\" value=\"re: "); + out.write(HTMLRenderer.sanitizeTagParam(subject)); + out.write("\" />"); + out.write("<tr class=\"postReply\">\n"); + out.write("<td colspan=\"3\">Reply: (<a href=\"smlref.jsp\" title=\"SML cheatsheet\">SML reference</a>)</td>\n</tr>\n"); + out.write("<tr class=\"postReplyText\">\n"); + out.write("<td colspan=\"3\"><textarea name=\"entrytext\" rows=\"2\" cols=\"100\"></textarea></td>\n"); + out.write("</tr>\n"); + out.write("<tr class=\"postReplyOptions\">\n"); + out.write(" <td colspan=\"3\">\n"); + out.write(" <input type=\"submit\" value=\"Preview...\" name=\"Post\" />\n"); + out.write(" Tags: <input type=\"text\" size=\"10\" name=\"entrytags\" />\n"); + out.write(" in a new thread? <input type=\"checkbox\" name=\"replyInNewThread\" />\n"); + out.write(" allow replies? <input type=\"checkbox\" name=\"allowReplies\" checked=\"true\" />\n"); + out.write(" attachment: <input type=\"file\" name=\"entryfile0\" />\n"); + out.write(" </td>\n</tr>\n</form>\n"); + out.write("<!-- body reply end -->\n"); + } + } + out.write("<!-- body end -->\n"); + } + private void renderThreadNav(User user, HttpServletRequest req, PrintWriter out, int threadOffset, ThreadIndex index) throws IOException { + out.write("<tr class=\"threadNav\" id=\"threads\"><td colspan=\"2\" nowrap=\"true\">\n"); + out.write("<!-- thread nav begin -->\n"); + out.write("<a href=\""); + out.write(getNavLink(req, 0)); + out.write("\"><< First Page</a> "); + if (threadOffset > 0) { + out.write("<a href=\""); + int nxt = threadOffset - 10; + if (nxt < 0) + nxt = 0; + out.write(getNavLink(req, nxt)); + out.write("\">< Prev Page</a>\n"); + } else { + out.write("< Prev Page\n"); + } + out.write("</td><td class=\"threadNavRight\" nowrap=\"true\">\n"); + + int max = index.getRootCount(); + if (threadOffset + 10 > max) { + out.write("Next Page> Last Page>>\n"); + } else { + out.write("<a href=\""); + out.write(getNavLink(req, threadOffset + 10)); + out.write("\">Next Page></a> <a href=\""); + out.write(getNavLink(req, -1)); + out.write("\">Last Page>></a>\n"); + } + out.write("<!-- thread nav end -->\n"); + out.write("</td></tr>\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("<!-- threads begin -->\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("<!-- thread begin node=" + node + " curRoot=" + curRoot + " threadOffset=" + threadOffset + " -->\n"); + renderThread(user, out, index, archive, req, node, 0, visibleEntry, state); + out.write("<!-- thread end -->\n"); + } + out.write("<!-- threads begin -->\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("<!-- threads begin -->\n"); + if (threadOffset + numThreads > index.getRootCount()) + numThreads = index.getRootCount() - threadOffset; + TreeRenderState state = new TreeRenderState(ignored); + + Collection requestedTags = getFilteredTags(req); + out.write("<!-- requested tags: " + requestedTags + " -->\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("<!-- thread begin (" + curRoot + ", " + writtenThreads + ", " + skipped + ", " + state.getRowsWritten() + ", " + isIgnored + ", " + threadOffset + ") -->\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("<!-- thread end -->\n"); + } + out.write("<!-- threads begin -->\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("<tr class=\"threadEven\">\n"); + else + out.write("<tr class=\"threadOdd\">\n"); + + out.write("<td class=\"threadFlag\">"); + out.write(getFlagHTML(user, node)); + out.write("</td>\n<td class=\"threadLeft\">\n"); + for (int i = 0; i < depth; i++) + out.write("<img src=\"images/threadIndent.png\" alt=\"\" border=\"0\" />"); + + 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("<a href=\""); + out.write(getCollapseLink(req, node)); + out.write("\" title=\"collapse thread\"><img border=\"0\" src=\"images/collapse.png\" alt=\"-\" /></a>\n"); + } else { + out.write("<a href=\""); + out.write(getExpandLink(req, node)); + out.write("\" title=\"expand thread\"><img border=\"0\" src=\"images/expand.png\" alt=\"+\" /></a>\n"); + } + } else { + out.write("<img src=\"images/noSubthread.png\" alt=\"\" border=\"0\" />\n"); + } + + out.write("<a href=\""); + out.write(getProfileLink(req, node.getEntry().getKeyHash())); + out.write("\" title=\"View the user's profile\">"); + + 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("</a>\n"); + + if (isFavorite) { + out.write("<img src=\"images/favorites.png\" alt=\"favorites\" border=\"0\" />\n"); + } else { + if (user.getAuthenticated()) { + // give them a link to bookmark or ignore the peer + out.write("(<a href=\""); + out.write(getAddToGroupLink(req, node.getEntry().getKeyHash(), user, FilteredThreadIndex.GROUP_FAVORITE)); + out.write("\" title=\"Add as a friend\"><img src=\"images/addToFavorites.png\" alt=\"friend\" border=\"0\" /></a>\n"); + out.write("/<a href=\""); + out.write(getAddToGroupLink(req, node.getEntry().getKeyHash(), user, FilteredThreadIndex.GROUP_IGNORE)); + out.write("\" title=\"Add to killfile\"><img src=\"images/addToIgnored.png\" alt=\"ignore\" border=\"0\" /></a>)\n"); + } + } + + out.write(" @ "); + out.write("<a href=\""); + out.write(getViewPostLink(req, node, user, false)); + out.write("\" title=\"View post\">"); + 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("</a>\n</td><td class=\"threadRight\">\n"); + out.write("<a href=\""); + out.write(getViewThreadLink(req, node, user)); + out.write("\" title=\"View all posts in the thread\">view thread</a>\n"); + out.write("</td></tr>\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 "<img src=\"images/favorites.png\" border=\"0\" alt=\"flagged author posted in the thread\" />"; + 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 = "<html>\n" + +"<head>\n" + +"<title>Syndie</title>\n" + +"<style>\n" + +".overallTable {\n" + +" border-spacing: 0px;\n" + +" border-width: 0px;\n" + +" border: 0px;\n" + +" margin: 0px;\n" + +" padding: 0px;\n" + +"}\n" + +".topNav {\n" + +" background-color: #BBBBBB;\n" + +"}\n" + +".topNav_user {\n" + +" text-align: left;\n" + +" float: left;\n" + +" align: left;\n" + +" display: inline;\n" + +"}\n" + +".topNav_admin {\n" + +" text-align: right;\n" + +" float: right;\n" + +" align: right;\n" + +" display: inline;\n" + +"}\n" + +".controlBar {\n" + +" background-color: #BBBBBB;\n" + +"}\n" + +".controlBarRight {\n" + +" text-align: right;\n" + +"}\n" + +".threadEven {\n" + +" background-color: #FFFFFF;\n" + +" white-space: nowrap;\n" + +"}\n" + +".threadOdd {\n" + +" background-color: #EEEEEE;\n" + +" white-space: nowrap;\n" + +"}\n" + +".threadLeft {\n" + +" text-align: left;\n" + +" align: left;\n" + +"}\n" + +".threadRight {\n" + +" text-align: right;\n" + +"}\n" + +".threadNav {\n" + +" background-color: #BBBBBB;\n" + +"}\n" + +".threadNavRight {\n" + +" text-align: right;\n" + +"}\n" + +".postMeta {\n" + +" background-color: #BBBBFF;\n" + +"}\n" + +".postMetaSubject {\n" + +" text-align: left;\n" + +"}\n" + +".postMetaLink {\n" + +" text-align: right;\n" + +"}\n" + +".postDetails {\n" + +" background-color: #DDDDFF;\n" + +"}\n" + +".postReply {\n" + +" background-color: #BBBBFF;\n" + +"}\n" + +".postReplyText {\n" + +" background-color: #BBBBFF;\n" + +"}\n" + +".postReplyOptions {\n" + +" background-color: #BBBBFF;\n" + +"}\n" + +"</style>\n" + +"</head>\n" + +"<body>\n" + +"<table border=\"0\" width=\"100%\" class=\"overallTable\">\n"; + + private static final String CONTROL_BAR_WITHOUT_TAGS = "<form>\n" + +"<tr class=\"controlBar\"><td colspan=\"2\">\n" + +"<!-- control bar begin -->\n" + +"Filter: <select disabled=\"true\" name=\"filter\">\n" + +" <option value=\"all\">All posts in all threads</option>\n" + +" <option value=\"self\">Threads you have posted in</option>\n" + +" <option value=\"favorites\">Threads your friends have posted in</option>\n" + +" </select>\n" + +"<input type=\"submit\" name=\"action\" value=\"Go\" />\n" + +"</td><td class=\"controlBarRight\"><a href=\"#threads\" title=\"Jump to the thread navigation\">Threads</a></td>\n" + +"<!-- control bar end -->\n" + +"</tr>\n" + +"</form>\n"; + + private static final String BODY = "<!-- body begin -->\n" + +"<!-- body meta begin -->\n" + +"<tr class=\"postMeta\">\n" + +" <td colspan=\"2\" class=\"postMetaSubject\">This is my subject</td>\n" + +" <td class=\"postMetaLink\">\n" + +" <a href=\"profile.jsp?blog=ovp\" title=\"View the author's profile\">petname</a> @ 2005/11/08\n" + +" <a href=\"?permalink\" title=\"Select a sharable link directly to this post\">permalink</a>\n" + +" </td>\n" + +"</tr>\n" + +"<!-- body meta end -->\n" + +"<!-- body post begin -->\n" + +"<tr class=\"postData\">\n" + +" <td colspan=\"3\">\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" + +" </td>\n" + +"</tr>\n" + +"<!-- body post end -->\n" + +"<!-- body details begin -->\n" + +"<tr class=\"postDetails\">\n" + +" <form action=\"viewattachment.jsp\" method=\"GET\">\n" + +" <td colspan=\"3\">\n" + +" External links:\n" + +" <a href=\"external.jsp?foo\" title=\"View foo.i2p\">http://foo.i2p/</a>\n" + +" <a href=\"external.jsp?bar\" title=\"View bar.i2p\">http://bar.i2p/</a>\n" + +" <br />\n" + +" Attachments: <select name=\"attachment\">\n" + +" <option value=\"0\">sampleRawSML.sml: Sample SML file with headers (4KB, type text/plain)</option>\n" + +" </select> <input type=\"submit\" name=\"action\" value=\"Download\" />\n" + +" <br /><a href=\"\" title=\"Expand the entire thread\">Full thread</a>\n" + +" <a href=\"\" title=\"Previous post in the thread\">Prev in thread</a> \n" + +" <a href=\"\" title=\"Next post in the thread\">Next in thread</a> \n" + +" </td>\n" + +" </form>\n" + +"</tr>\n" + +"<!-- body details end -->\n" + +"<!-- body reply begin -->\n" + +"<form action=\"post.jsp\" method=\"POST\">\n" + +"<tr class=\"postReply\">\n" + +" <td colspan=\"3\">Reply: (<a href=\"smlref.jsp\" title=\"SML cheatsheet\">SML reference</a>)</td>\n" + +"</tr>\n" + +"<tr class=\"postReplyText\">\n" + +" <td colspan=\"3\"><textarea name=\"smltext\" rows=\"2\" cols=\"100\"></textarea></td>\n" + +"</tr>\n" + +"<tr class=\"postReplyOptions\">\n" + +" <td colspan=\"3\">\n" + +" <input type=\"submit\" value=\"Preview...\" name=\"action\" />\n" + +" Tags: <input type=\"text\" size=\"10\" name=\"tags\" />\n" + +" in a new thread? <input type=\"checkbox\" name=\"replyInNewThread\" />\n" + +" allow replies? <input type=\"checkbox\" name=\"allowReplies\" checked=\"true\" />\n" + +" attachment: <input type=\"file\" name=\"entryfile0\" />\n" + +" </td>\n" + +"</tr>\n" + +"</form>\n" + +"<!-- body reply end -->\n" + +"<!-- body end -->\n"; + + + private static final String END_HTML = "</table>\n" + +"</body>\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_bxCyD<C*!3BGlPX>x`7I;J! zGca%qgD@k*tT_@u!Ofm7jv*44dnf4fH3tZ^>_4tKE9Clj4i1rJD<4YCX*3oP;ar!n z)5-rsP;`KXfCW#UR@}n<nY&J=rJGN_c}C)$>QXx%!<fEzo7N{zmJ*&{IYBA@mgl3j z<&%`^Lx1^IsV(34{NC0G?%TQ%+>c)r#4>L=AfVhj>4L=~oATm$vhQ}enqKkxb9bT5 zbG|MnzvWluYxx~F<ovEOHTHc`;NaP`>eR#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_bxCyD<C*!3BGlPX>x`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<! z6@3CLTPu<AH}@sHuz%*x%$ak}eBYbH&B4y>#!S-AdbK#>xBt(!xA^}2>&`pr(%-{} z$4kk%kwQwTWRjW8Wg$yhb>2<x@{p&zx;9MVicqAYx;0Jd%21}VKvJ2?RiR210oXu; zMKjgabNaYhn59|uSR!}xFi-Pp79_(h!XhoINl}|_8J1~TaHQ4Bt->m;m<nsaAPq}s zkz+8}K!ZhdwV;kU;hyf*`WQ<T;gKHIa$!L-!!tb#^^l_0E4<PRdH4ZMy0HWOkU$}< z0fRIwDTN1v!3G+vYUesl%*lvqBhklLqRhy`SLDKiWK>4sBTi!gyeP+L)M6!mK$C9l zfD?jI2y4I~O((SA!C<g~u3k}BlP2aA8^cKSF_tK+dVyp8&0=h{_c@l-q=xUk-eF~P z9PaeChv23>H1zTZtyVB50nOvUmM{-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<SDPN4O6%x6sf3gO;frul&LI`RHkxOs8U4$ zHqc<vOm+2~K5iCfX;wX!$lW~5)4ZAm$#9FXNQ-Jx)TUd8Wm*;-X|-~zuu3bY!WuA0 z!xCEL7z{ShV9{JHsAEpJr+c+N#u7z%q(`+}Sdh%{OwU3+q^R`@uk=D5en69M>_9&x zPzY<lAPq}O;lW_Afd;GExlR*vGNRf@^f8twGqUg%xv(G^l~MSJ(-;6R$}t+XScxCd zq#HZngkTiH8Zb!H2`zXq7;K=cSJc&{i8;l_FcN)?CCaK^;IRH?F>JK=IV`D34Zin! z2P>Q7;7)IQ5Zsgp4ZZw<Rx21L0nOvUmM{-Z!zgb4!#UZEzw$DSHixUny8H7x&y7aU z9_-Kb_QKT#!NpgLC$FXL%l#SY^wTbv<IVEXeEe;Dd}(^){nL{dT<tAq_l~yCp09QM k@@#jlTYsikuG{kVNN;{$+dKVn^CLxC&R4U~_t(e&0NsJy6aWAK literal 0 HcmV?d00001 diff --git a/apps/syndie/jsp/images/favorites.png b/apps/syndie/jsp/images/favorites.png new file mode 100644 index 0000000000000000000000000000000000000000..5fa5a83bb3356281a22a77ba14d2eb1f3fc5fe66 GIT binary patch literal 463 zcmeAS@N?(olHy`uVBq!ia0vp^AT}2V8<6ZZI=>f4u_bxCyD<C*!3BGlPX>x`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(<pl+=1+#3d`nsJis%wcpjpufqZ(-HPts3BK)pX0dT_^oQ zX}8A_rP*6oTzyqjmZf-F5+uC%Y;Ki`r@)n*u<%?b#o5!QS@HAU_L#H)C@fW9Bf^@r zF@App&%OH#BUa@f-~3dUSw(2W{{PiFQxlRlojX5m`<)r5clVz6KWl7U?N@W~nHsZg zqJ>4nSzdKfZ{_Lb(!5vKZ~ZyxX062AdKp>ie5dQ7pIE1^%oI((%<)=U`}Xwkxpth> zD;V@l{%1SKub2F`O`5UcxO)A+Uv7KPN}e@U_{3ZDYnr6s<tAXrGcb6%`njxgN@xNA D9^SlV literal 0 HcmV?d00001 diff --git a/apps/syndie/jsp/images/noSubthread.png b/apps/syndie/jsp/images/noSubthread.png new file mode 100644 index 0000000000000000000000000000000000000000..3ea13d3a131c793fc3cabe91a9861d69767a2164 GIT binary patch literal 129 zcmeAS@N?(olHy`uVBq!ia0vp^AT}2V8<6ZZI=>f4u_bxCyD<C*!3BGlPX>x`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_bxCyD<C*!3BGlPX>x`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 0000000000..90c07f0777 --- /dev/null +++ b/apps/syndie/jsp/index.html @@ -0,0 +1,3 @@ +<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"><html +><head><title>Syndie</title></head +><body><meta http-equiv="refresh" content="0;url=index.jsp" /><a href="index.jsp">Enter</a></body></html> diff --git a/apps/syndie/jsp/switchuser.jsp b/apps/syndie/jsp/switchuser.jsp new file mode 100644 index 0000000000..64a31219f8 --- /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"); +%><!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 TRANSITIONAL//EN" "http://www.w3c.org/TR/1999/REC-html401-19991224/loose.dtd"> +<html> +<head> +<title>Syndie</title> +<link href="style.jsp" rel="stylesheet" type="text/css" > +</head> +<body> +<form action="threads.jsp" method="GET"> +Syndie login: <input type="text" name="login" /><br /> +Password: <input type="password" name="password" /><br /> +<input type="submit" name="action" value="Login" /> +<input type="submit" name="action" value="Cancel" /> +</form> +</body> \ No newline at end of file diff --git a/apps/syndie/jsp/web.xml b/apps/syndie/jsp/web.xml index 1f5798c835..acdb2e131b 100644 --- a/apps/syndie/jsp/web.xml +++ b/apps/syndie/jsp/web.xml @@ -14,18 +14,25 @@ <servlet-class>net.i2p.syndie.web.RSSServlet</servlet-class> </servlet> + <servlet> + <servlet-name>net.i2p.syndie.web.ViewThreadedServlet</servlet-name> + <servlet-class>net.i2p.syndie.web.ViewThreadedServlet</servlet-class> + </servlet> + <servlet> <servlet-name>net.i2p.syndie.UpdaterServlet</servlet-name> <servlet-class>net.i2p.syndie.UpdaterServlet</servlet-class> - <load-on-startup>1</load-on-startup> - </servlet> + <load-on-startup>1</load-on-startup> + </servlet> <!-- precompiled servlets --> + <!-- <servlet-mapping> <servlet-name>net.i2p.syndie.jsp.index_jsp</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping> + --> <servlet-mapping> <servlet-name>net.i2p.syndie.web.ArchiveServlet</servlet-name> <url-pattern>/archive/*</url-pattern> @@ -34,6 +41,10 @@ <servlet-name>net.i2p.syndie.web.RSSServlet</servlet-name> <url-pattern>/rss.jsp</url-pattern> </servlet-mapping> + <servlet-mapping> + <servlet-name>net.i2p.syndie.web.ViewThreadedServlet</servlet-name> + <url-pattern>/threads.jsp</url-pattern> + </servlet-mapping> <session-config> <session-timeout> -- GitLab