/* A UI subcomponent to load a list of other versions of this wiki via FMS.
*
* Copyright (C) 2010, 2011 Darrell Karbott
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation; either
* version 2.0 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*
* Author: djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks
*
* This file was developed as component of
* "fniki" (a wiki implementation running over Freenet).
*/
package fniki.wiki.child;
import java.io.IOException;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static ys.wikiparser.Utils.*;
import fmsutil.FMSUtil;
import wormarc.ExternalRefs;
import wormarc.FileManifest;
import fniki.wiki.ArchiveManager;
import fniki.wiki.ChildContainer;
import fniki.wiki.ChildContainerException;
import fniki.wiki.GraphLog;
import static fniki.wiki.HtmlUtils.*;
import fniki.wiki.WikiContext;
public class LoadingVersionList extends AsyncTaskContainer {
private StringBuilder mListHtml = new StringBuilder();
private String mName = "";
private String mContainerPrefix;
// Has no parent.
private final static String BASE_VERSION = "0000000000000000";
// We don't know parent.
private final static String UNKNOWN_VERSION = "???";
public LoadingVersionList(ArchiveManager archiveManager) {
super(archiveManager);
}
public synchronized String getListHtml() {
return mListHtml.toString();
}
public String handle(WikiContext context) throws ChildContainerException {
try {
if (context.getAction().equals("confirm")) {
// Copy stuff we need out because context isn't threadsafe.
mName = context.getPath();
mContainerPrefix = context.getString("container_prefix", null);
if (mContainerPrefix == null) {
throw new RuntimeException("Assertion Failure: mContainerPrefix == null");
}
startTask();
try {
Thread.sleep(1000); // Hack. Give task thread a chance to finish.
} catch (InterruptedException ioe) {
/* NOP */
}
sendRedirect(context, context.getPath());
return "unreachable code";
}
boolean showBuffer = false;
String confirmTitle = null;
String cancelTitle = null;
String title = null;
switch (getState()) {
case STATE_WORKING:
showBuffer = true;
title = "Loading Wiki Version Info from FMS";
cancelTitle = "Cancel";
break;
case STATE_WAITING:
// Shouldn't hit this state.
showBuffer = false;
title = "Load Wiki Version Info from FMS";
confirmTitle = "Load";
cancelTitle = "Cancel";
break;
case STATE_SUCCEEDED:
showBuffer = true;
title = "Loaded Wiki Version Info from FMS";
confirmTitle = null;
cancelTitle = "Done";
break;
case STATE_FAILED:
showBuffer = true;
title = "Full Read of Wiki Version Info Failed";
confirmTitle = "Reload";
cancelTitle = "Done";
break;
}
StringWriter buffer = new StringWriter();
PrintWriter body = new PrintWriter(buffer);
body.println("<html><head>\n");
body.println(metaRefresh());
body.println("<style type=\"text/css\">\n");
body.println("TD{font-family: Arial; font-size: 7pt;}\n");
body.println("</style>\n");
body.println("<title>" + escapeHTML(title) + "</title>\n");
body.println("</head><body>\n");
body.println("<h3>" + escapeHTML(title) + "</h3>");
body.println(String.format("wikiname:%s<br>FMS group:%s<p>",
escapeHTML(context.getString("wikiname", "NOT_SET")),
escapeHTML(context.getString("fms_group", "NOT_SET"))));
if (showBuffer) {
body.println(getListHtml());
body.println("<hr>");
body.println("<pre>");
body.print(escapeHTML(getOutput()));
body.println("</pre>");
}
body.println("<hr>");
addButtonsHtml(context, body, confirmTitle, cancelTitle);
body.println("</body></html>");
body.close();
return buffer.toString();
} catch (IOException ioe) {
context.logError("LoadingVersionList", ioe);
return "Error LoadingVersionList";
}
}
// Doesn't need escaping.
public static String trustString(int value) {
if (value == -1) {
return "null";
}
return Integer.toString(value);
}
// LATER: move and document better.
// uri format:
// <sha1_hash_of_manifest><underbar><sha1_hash_of_parent_uri>[<underbar><sha1_hash_of_rebase_uri>]
//
// First is parent version, optional second is rebase version.
public static String[] getParentVersions(FMSUtil.BISSRecord record) {
if (record.mKey == null) {
return new String[] {UNKNOWN_VERSION};
}
String[] fields = record.mKey.split("/");
if (fields.length != 2) {
return new String[] {UNKNOWN_VERSION};
}
fields = fields[1].split("_");
if (fields.length < 2) { // LATER. handle multiple parents
if (fields.length == 1 && fields[0].length() == 16) {
// Assume the entry is the first version.
return new String[] {BASE_VERSION};
}
return new String[] {UNKNOWN_VERSION};
}
// LATER: tighten up.
if (fields.length > 2) {
return new String[] {fields[1], fields[2]};
}
return new String[] {fields[1]};
}
final static class DAGData implements Comparable<DAGData> {
public final int mSize;
public final long mEpochMs;
public final List<GraphLog.DAGNode> mDag;
DAGData(int size, long epochMs, List<GraphLog.DAGNode> dag) {
mSize = size;
mEpochMs = epochMs;
mDag = dag;
}
public int compareTo(DAGData o) {
if (o == null) { throw new NullPointerException(); }
if (o == this) { return 0; }
if (o.mSize - mSize != 0) { // first by descending size.
return o.mSize - mSize;
}
if (o.mEpochMs - mEpochMs != 0) { // then by descending date.
return (o.mEpochMs - mEpochMs) > 0 ? 1: -1;
}
return 0;
}
// Hmmmm... not sure these are required.
public boolean equals(Object obj) {
if (obj == this) { return true; }
if (obj == null || (!(obj instanceof DAGData))) {
return false;
}
DAGData other = (DAGData)obj;
return mSize == other.mSize &&
mEpochMs == other.mEpochMs &&
mDag.equals(other.mDag);
}
public int hashCode() {
int result = 17;
result = 37 * mSize;
result = 37 * (int)(mEpochMs ^ (mEpochMs >>> 32));
result = 37 * mDag.hashCode();
return result;
}
}
// Wed, 02 Mar 11 02:57:38 -0000
private final static DateFormat sDateFormat = new java.text.SimpleDateFormat("EEE, dd MMM yy HH:mm:ssZ");
private static void sortBySizeAndDate(List<List<GraphLog.DAGNode>> dags, Map<String, List<FMSUtil.BISSRecord>> lut) {
List<DAGData> dagData = new ArrayList<DAGData>();
for (List<GraphLog.DAGNode> dag : dags) {
long epochMs = 0;
for (GraphLog.DAGNode node : dag) {
for (FMSUtil.BISSRecord record : lut.get(node.mTag)) {
try {
long zuluMs = sDateFormat.parse(record.mDate).getTime();
if (zuluMs > epochMs) {
epochMs = zuluMs;
}
} catch (ParseException pe) {
System.err.println("Parse of date failed: " + record.mDate);
}
}
}
dagData.add(new DAGData(dag.size(), epochMs, dag));
}
Collections.sort(dagData);
dags.clear();
for (DAGData data : dagData) {
dags.add(data.mDag);
}
}
public synchronized String getRevisionGraphHtml(List<FMSUtil.BISSRecord> records)
throws IOException {
// Build a list of revision graph edges from the NNTP notification records.
List<GraphLog.GraphEdge> edges = new ArrayList<GraphLog.GraphEdge>();
Map<String, List<FMSUtil.BISSRecord>> lut = new HashMap<String, List<FMSUtil.BISSRecord>>();
for (FMSUtil.BISSRecord record : records) {
String child = getVersionHex(record.mKey);
String[] parents = getParentVersions(record);
if (child.equals(UNKNOWN_VERSION) || parents[0].equals(UNKNOWN_VERSION)) {
System.err.println(String.format("Skipping: (%s, %s)", child, parents[0]));
System.err.println(" " + record.mKey);
continue;
}
if (child.equals(BASE_VERSION)) {
// INTENT: cycles in the DAG (i.e. non-dag) break the drawing code. Catch sleazy stuff.
System.err.println(String.format("Attempted attack? Skipping: (%s, %s)", child, parents[0]));
System.err.println(" " + record.mKey);
continue;
}
List<FMSUtil.BISSRecord> recordsEntry = lut.get(child);
if (recordsEntry == null) {
recordsEntry = new ArrayList<FMSUtil.BISSRecord>();
lut.put(child, recordsEntry);
}
if (!lut.get(child).contains(record)) {
lut.get(child).add(record);
}
for (String parent : parents) { // add edges for both parent and rebase versions
// Think of the graph as going from the bottom "up".
// Edges point from parent version to child version.
GraphLog.GraphEdge edge = new GraphLog.GraphEdge(parent, child);
if (!edges.contains(edge)) { // hmmmm.... O(n) search. Dude, that's the least of your worries.
edges.add(edge);
}
}
}
// Passing BASE_VERSION keep the drawing code from drawing '|'
// below root nodes.
List<List<GraphLog.DAGNode>> dags = GraphLog.build_dags(edges, BASE_VERSION);
sortBySizeAndDate(dags, lut);
// Draw the revision graph(s).
StringWriter out = new StringWriter();
out.write("<pre>\n");
for (List<GraphLog.DAGNode> dag : dags) {
out.write("<hr>\n");
List<Integer> seen = new ArrayList<Integer>();
GraphLog.AsciiState state = GraphLog.asciistate();
for (GraphLog.DAGNode value : dag) {
List<FMSUtil.BISSRecord> references = lut.get(value.mTag);
List<String> lines = new ArrayList<String>();
String versionLink = getShortVersionLink(mContainerPrefix, "/jfniki/loadarchive",
references.get(0).mKey); // All the same.
String rebaseLink = getRebaseLink(mContainerPrefix, "/jfniki/loadarchive",
references.get(0).mKey, "finished",
"[rebase]", false);
lines.add(versionLink + " " + rebaseLink);
for (FMSUtil.BISSRecord reference : references) {
// LATER: Sort by date
lines.add(String.format("user: %s (%s, %s, %s, %s)",
reference.mFmsId,
trustString(reference.msgTrust()),
trustString(reference.trustListTrust()),
trustString(reference.peerMsgTrust()),
trustString(reference.peerTrustListTrust())
));
lines.add(String.format("date: %s", reference.mDate)); // Reliable?
}
String[] parentsAgain = getParentVersions(references.get(0));
if (parentsAgain.length == 2) {
lines.add(escapeHTML(String.format("rebased: %s (UNVERIFIED!)", parentsAgain[1])));
}
lines.add("");
GraphLog.ascii(out, state, "o", lines, GraphLog.asciiedges(seen, value.mId, value.mParentIds));
}
}
out.write("</pre>\n");
out.flush();
return out.toString();
}
public boolean doWork(PrintStream out) throws Exception {
synchronized (this) {
mListHtml = new StringBuilder();
}
try {
out.println("Reading versions from FMS...");
String graphHtml = getRevisionGraphHtml(mArchiveManager.getRecentWikiVersions(out));
synchronized (this) {
mListHtml.append(graphHtml);
}
return true;
} catch (IOException ioe) {
out.println("Error reading log: " + ioe.getMessage());
return false;
}
}
}