/*
* #!
* Ontopia Navigator
* #-
* Copyright (C) 2001 - 2013 The Ontopia Project
* #-
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* !#
*/
package net.ontopia.topicmaps.nav2.utils;
import java.io.IOException;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.jsp.PageContext;
import net.ontopia.topicmaps.core.TMObjectIF;
import net.ontopia.topicmaps.core.TopicIF;
import net.ontopia.topicmaps.core.TopicMapIF;
import net.ontopia.topicmaps.nav2.core.NavigatorPageIF;
import net.ontopia.topicmaps.nav2.core.NavigatorRuntimeException;
import net.ontopia.topicmaps.query.core.InvalidQueryException;
import net.ontopia.topicmaps.query.core.ParsedQueryIF;
import net.ontopia.topicmaps.query.core.QueryProcessorIF;
import net.ontopia.topicmaps.query.core.QueryResultIF;
import net.ontopia.topicmaps.query.utils.QueryUtils;
import net.ontopia.topicmaps.utils.TopicTreeNode;
import net.ontopia.utils.OntopiaRuntimeException;
import net.ontopia.utils.StringUtils;
import net.ontopia.utils.ObjectUtils;
/**
* EXPERIMENTAL: This class can output a nice collapsing/expanding
* tree view of a topic map implemented with DHTML, which uses tolog
* queries to produce the tree. The class is configurable in various
* ways, and can also be subclassed to further fine-tune the
* rendering.
*
* @since 3.0
*/
public class DynamicTreeWidget {
protected static final int OPEN = 1;
protected static final int CLOSE = 2;
protected static final int EXPAND_ALL = 3;
protected static final int CLOSE_ALL = 4;
protected static final int WINDOW_SIZE = 100;
protected QueryProcessorIF processor;
protected String nodepage;
protected String staticurl;
protected String imageurl = "";
protected int windowSize = WINDOW_SIZE;
protected NavigatorPageIF context;
private String name = "ONTOPIA-WIDGET-ATTRIBUTE";
private ParsedQueryIF query;
private TopicMapIF topicmap;
private Map map = new HashMap();
private String topquery;
protected String ownpage;
private String querystr; // string repr of query, parsed into 'query'
private String nodeFrame;
private boolean addAnchor = true;
private String tablequery;
private String dataquery;
private java.util.Comparator childrenComparator = new java.util.Comparator() {
private java.util.Comparator c = net.ontopia.topicmaps.utils.TopicComparators.getTopicNameComparator(java.util.Collections.EMPTY_SET);
public int compare(Object o1, Object o2) {
return c.compare(((net.ontopia.topicmaps.utils.TopicTreeNode)o1).getTopic(),
((net.ontopia.topicmaps.utils.TopicTreeNode)o2).getTopic());
}
};
private Collection pNodes = new HashSet();
private String[] dependentWidgets;
private HttpServletRequest request;
private boolean debug;
// --- External interface
/**
* PUBLIC: Sets up the widget ready for use.
*
* @param topicmap The topic map being displayed.
* @param tablequery A tolog query that generates the entire tree. The
* query must have at least two columns, where the first column
* contains all the parent nodes and the second column contains the
* children of those parents. Any further columns will be used to
* populate the data attribute of the tree nodes.
* @param ownpage The URL of the page the widget is on. The widget
* will append request parameters in the form "a=b&c=d&e=f..."
* @param nodepage The URL of of the page that shows the nodes.
*/
public DynamicTreeWidget(TopicMapIF topicmap,
String tablequery,
String ownpage, String nodepage) throws InvalidQueryException {
setTopicMap(topicmap);
this.tablequery = tablequery;
this.ownpage = ownpage;
this.nodepage = nodepage;
}
/**
* PUBLIC: Sets up the widget ready for use.
*/
public DynamicTreeWidget() {
}
/**
* PUBLIC: Sets the topic map used by the widget.
*/
public void setTopicMap(TopicMapIF topicmap) {
this.topicmap = topicmap;
this.processor = QueryUtils.getQueryProcessor(topicmap);
}
/**
* PUBLIC: Sets the tolog query that generates the entire tree. The
* query must have at least two columns, where the first column
* contains all the parent nodes and the second column contains the
* children of those parents. Any further columns will be used to
* populate the data attribute of the tree nodes.
*/
public void setTableQueryString(String tablequery) {
this.tablequery = tablequery;
}
/**
* PUBLIC: The URL of the page the widget is on. The widget will
* append request parameters in the form "a=b&c=d&e=f..."
*/
public void setOwnPageUrl(String ownPageUrl) {
this.ownpage = ownPageUrl;
}
/**
* PUBLIC: The URL of of the page that shows the nodes.
*/
public void setNodePageUrl(String nodePageUrl) {
this.nodepage = nodePageUrl;
}
/**
* INTERNAL: Debug flag. Used only for debugging purposes.
*/
public void setDebug(boolean debug) {
this.debug = debug;
}
/**
* PUBLIC: The name of the session key in which the set of open nodes is
* stored. Using the same session key for different widgets will make them
* share open/closed information.
*/
public void setWidgetName(String name) {
this.name = escapeName(name);
}
/**
* PUBLIC: The name of the session key in which the set of open nodes is
* stored. Using the same session key for different widgets will make them
* share open/closed information.
*/
public void setDependentWidgets(String[] widget_names) {
this.dependentWidgets = new String[widget_names.length];
for (int i=0; i < dependentWidgets.length; i++) {
dependentWidgets[i] = escapeName(widget_names[i]);
}
}
protected String escapeName(String name) {
return StringUtils.replace(name, "-", "_");
}
/**
* PUBLIC: The URL at which the graphics used by the widget are found. The
* widget will produce HTML like <img src="[imageurl]spacer.gif"> to
* refer to the graphics.
*/
public void setImageUrl(String imageurl) {
this.imageurl = imageurl;
}
/**
* PUBLIC: If set to true the widget will add anchors on all links that
* open/close nodes in the tree. The default is true.
*/
public void setAddAnchor(boolean addAnchor) {
this.addAnchor = addAnchor;
}
/**
* PUBLIC: Sets the maximum number of nodes displayed by the widget at once.
* If the number of nodes to display exceeds the maximum the widget will break
* the display into multiple "pages".
*/
public void setWindowSize(int windowSize) {
this.windowSize = windowSize;
}
/**
* PUBLIC: The name of the HTML frame in which to open links to nodes.
*/
public void setNodeFrame(String nodeFrame) {
this.nodeFrame = nodeFrame;
}
/**
* PUBLIC: Runs the widget, producing the output.
*/
public void run(PageContext ctxt, Writer writer) throws IOException,
InvalidQueryException, NavigatorRuntimeException {
run((HttpServletRequest) ctxt.getRequest(), writer);
}
/**
* EXPERIMENTAL: The name of the HTML frame in which to open links
* to nodes.
*
* @since 2.0.3
*/
public void setTableQuery(String tablequery) {
this.tablequery = tablequery;
}
public void setChildrenComparator(java.util.Comparator childrenComparator) {
this.childrenComparator = childrenComparator;
}
public void setTopQuery(String topquery) {
if (topquery != null && "".equals(topquery.trim()))
this.topquery = null;
else
this.topquery = topquery;
}
public void setDataQuery(String dataquery) {
this.dataquery = dataquery;
}
/**
* PUBLIC: Runs the widget, producing the output.
*
* @since 2.2.1
*/
public void run(HttpServletRequest request, Writer writer)
throws IOException, InvalidQueryException, NavigatorRuntimeException {
initializeContext(request);
Map parameters = request.getParameterMap();
this.request = request;
// check that query has been parsed
if (query == null && querystr != null)
query = processor.parse(querystr, context.getDeclarationContext());
// get current node
TopicIF current = null;
if (parameters.containsKey("current"))
current = getTopic(get(parameters, "current"));
int action = getAction(parameters);
int topline = 0;
if (parameters.containsKey("topline")) {
try {
topline = Integer.parseInt(get(parameters, "topline"));
} catch (NumberFormatException e) {}
}
try {
doQuery(topline, writer);
} catch (InvalidQueryException e) {
throw new net.ontopia.utils.OntopiaRuntimeException(e);
}
this.request = null;
}
protected void initializeContext(HttpServletRequest request) {
if (context == null)
context = FrameworkUtils.getContextTag(request);
}
private void doQuery(int topline, Writer writer) throws IOException,
InvalidQueryException {
TopicTreeNode tree = buildTree();
writeHTML(tree, topline, writer);
}
// --- Logic
private TopicTreeNode buildTree() throws InvalidQueryException {
TopicTreeNode root = new TopicTreeNode(null);
if (tablequery != null) {
// child of root if no parent; leaf if no children
Map pmap = new HashMap(); // { pt : pn }
Map cmap = new HashMap(); // { ct : { cn : pn } }
// execute table query
QueryResultIF qr1 = processor.execute(tablequery);
while (qr1.next()) {
TopicIF parent = (TopicIF) qr1.getValue(0);
TopicIF child = (TopicIF)qr1.getValue(1);
Object[] data = null;
if (qr1.getWidth() > 2) {
data = new Object[qr1.getWidth()-2];
for (int i=0; i < data.length; i++) {
data[i] = qr1.getValue(2+i);
}
}
// use parentless node as top node if no top query given
if (topquery == null && parent == null)
registerNodes(root, child, data, pmap, cmap);
else
registerNodes(parent, child, data, pmap, cmap);
}
// execute top query
if (topquery != null) {
QueryResultIF qr2 = processor.execute(topquery);
while (qr2.next()) {
TopicIF child = (TopicIF)qr2.getValue(0);
Object[] data = null;
if (qr2.getWidth() > 1) {
data = new Object[qr2.getWidth()-1];
for (int i=0; i < data.length; i++) {
data[i] = qr2.getValue(1+i);
}
}
registerNodes(root, child, data, pmap, cmap);
}
}
// loop over parents and attach to root
Iterator iter = pmap.values().iterator();
while (iter.hasNext()) {
TopicTreeNode node = (TopicTreeNode)iter.next();
if (node.getParent() == null) {
continue;
//! node.setParent(root);
//! // run node data query because root nodes have no data attached
//! if (this.dataquery != null) {
//! Map params = Collections.singletonMap("topic", node.getTopic());
//! QueryResultIF qr3 = processor.execute(dataquery, params);
//! if (qr3.next()) {
//! Object[] data = new Object[qr3.getWidth()];
//! for (int i=0; i < data.length; i++) {
//! data[i] = qr3.getValue(i);
//! }
//! node.setAttribute("data", data);
//! }
//! }
}
if (node.getChildren().isEmpty())
node.setAttribute("action", null);
else {
node.setAttribute("action", "close");
// sort children
if (childrenComparator != null)
java.util.Collections.sort(node.getChildren(), childrenComparator);
}
}
// sort children of root node
if (childrenComparator != null)
java.util.Collections.sort(root.getChildren(), childrenComparator);
} else {
QueryResultIF result = processor.execute(topquery, context
.getDeclarationContext());
while (result.next()) {
TopicIF topic = (TopicIF) result.getValue(0);
TopicTreeNode group = new TopicTreeNode(topic);
group.setParent(root);
group.setAttribute("id", getId(topic));
process(group);
if (group.getChildren().isEmpty())
group.setAttribute("action", null);
else
group.setAttribute("action", "close");
}
}
return root;
}
private void registerNodes(TopicIF parent, TopicIF child, Object[] cndata, Map pmap, Map cmap) {
// parent node
TopicTreeNode pn = null;
if (parent != null) {
if (pmap.containsKey(parent)) {
pn = (TopicTreeNode)pmap.get(parent);
} else {
if (cmap.containsKey(parent)) {
// hoist from cmap
List cl = (List)cmap.get(parent);
pn = (TopicTreeNode)cl.get(0);
} else {
// create new pn
pn = new TopicTreeNode(parent);
pn.setAttribute("id", getId(parent));
}
pmap.put(parent, pn);
}
}
registerNodes(pn, child, cndata, pmap, cmap);
}
private void registerNodes(TopicTreeNode pn, TopicIF child, Object[] cndata, Map pmap, Map cmap) {
// child node
TopicIF parent = (pn == null ? null : pn.getTopic());
if (child != null) {
TopicTreeNode cn = null;
if (pmap.containsKey(child)) {
cn = (TopicTreeNode)pmap.get(child);
} else {
List cl = (List)cmap.get(child);
if (cl == null) {
cl = new ArrayList();
cmap.put(child, cl);
}
for (int i=0; i < cl.size(); i=i+2) {
TopicTreeNode c = (TopicTreeNode)cl.get(i);
TopicTreeNode p = (TopicTreeNode)cl.get(i+1);
if (ObjectUtils.equals(p.getTopic(), parent)) {
cn = c;
pmap.put(parent, p);
break;
}
}
if (cn == null) {
cn = new TopicTreeNode(child);
cn.setAttribute("id", getId(child));
cl.add(cn);
cl.add(pn);
}
}
// other columns retrieved as data array
if (cndata != null)
cn.setAttribute("data", cndata);
if (pn != null)
cn.setParent(pn);
}
}
private QueryResultIF getChildren(TopicIF topic) throws InvalidQueryException {
map.put("parent", topic);
return query.execute(map);
}
// --- Reusable
private void process(TopicTreeNode parent) throws InvalidQueryException {
TopicIF topic = parent.getTopic();
QueryResultIF children = getChildren(topic);
while (children.next()) {
TopicIF childtopic = (TopicIF) children.getValue(0);
TopicTreeNode child = new TopicTreeNode(childtopic);
child.setAttribute("id", getId(childtopic));
child.setParent(parent);
process(child);
if (!child.getChildren().isEmpty())
child.setAttribute("action", "close");
}
}
private void writeHTML(TopicTreeNode node, int topline, Writer writer)
throws IOException {
if (request.getAttribute("DynamicTreeWidget_javascriptWritten") == null) {
request.setAttribute("DynamicTreeWidget_javascriptWritten", Boolean.TRUE);
writer.write("<script>\n" +
" function expand_all(pnodes, widgetname) {\n" +
" for (i=0; i < pnodes.length; i++) {\n" +
" var id = widgetname + \"_\" + pnodes[i];\n" +
" var img = document.getElementById(id);\n" +
" if (img != null && img.src != null && img.src.indexOf(\"expand.gif\") != -1) {\n" +
" var ix = img.src.indexOf(\"expand.gif\");\n" +
" img.src = img.src.substring(0, ix) + \"collapse.gif\";\n" +
" }" +
" var span = document.getElementById(id + \"span\");\n" +
" if (span != null) { display_on(span); }\n" +
" }\n" +
" }\n" +
" function close_all(pnodes, widgetname) {\n" +
" for (i=0; i < pnodes.length; i++) {\n" +
" var id = widgetname + \"_\" + pnodes[i];\n" +
" var img = document.getElementById(id);\n" +
" if (img != null && img.src != null && img.src.indexOf(\"collapse.gif\") != -1) {\n" +
" var ix = img.src.indexOf(\"collapse.gif\");\n" +
" img.src = img.src.substring(0, ix) + \"expand.gif\";\n" +
" }" +
" var span = document.getElementById(id + \"span\");\n" +
" if (span != null) { display_off(span); }\n" +
" }\n" +
" }\n" +
" function open_close(elementid, widgetname) {\n" +
" var id = widgetname + \"_\" + elementid;\n" +
" element = document.getElementById(id);\n" +
" if (element != null) {\n" +
" if (element.src.indexOf(\"expand.gif\") != -1) {\n" +
" var ix = element.src.indexOf(\"expand.gif\");\n" +
" element.src = element.src.substring(0, ix) + \"collapse.gif\";\n" +
" } else {\n" +
" var ix = element.src.indexOf(\"collapse.gif\");\n" +
" element.src = element.src.substring(0, ix) + \"expand.gif\";\n" +
" }\n" +
" }\n" +
" switch_display(id + 'span');\n" +
" }\n" +
" function switch_display(elementid) {\n" +
" element = document.getElementById(elementid);\n" +
" if (element != null) {\n" +
" if (element.style.display == \"none\")\n" +
" element.style.display = \"\";\n" +
" else\n" +
" element.style.display = \"none\";\n" +
" }\n" +
" }\n" +
" function display_on(element) {\n" +
" element.style.display = \"\";\n" +
" }\n" +
" function display_off(element) {\n" +
" element.style.display = \"none\";\n" +
" }\n" +
"</script>\n");
}
writer.write("<script>\n" +
" var pNodes_" + name + ";\n");
// expand_all_*
writer.write(" function expand_all_" + name + "() {\n" +
" expand_all(pNodes_" + name + ", '" + name + "');\n");
if (dependentWidgets != null) {
for (int i=0; i < dependentWidgets.length; i++) {
writer.write(" expand_all(pNodes_" + dependentWidgets[i] + ", '" + dependentWidgets[i] + "');\n");
}
}
writer.write(" }\n");
// close_all_*
writer.write(" function close_all_" + name + "() {\n" +
" close_all(pNodes_" + name + ", '" + name + "');\n");
if (dependentWidgets != null) {
for (int i=0; i < dependentWidgets.length; i++) {
writer.write(" close_all(pNodes_" + dependentWidgets[i] + ", '" + dependentWidgets[i] + "');\n");
}
}
writer.write(" }\n");
// open_close_*
writer.write(" function open_close_" + name + "(elementid) {\n" +
" open_close(elementid, '" + name + "');\n");
if (dependentWidgets != null) {
for (int i=0; i < dependentWidgets.length; i++) {
writer.write(" open_close(elementid, '" + dependentWidgets[i] + "');\n");
}
}
writer.write(" }\n");
writer.write("</script>\n");
int nodes = countNodes(node);
staticurl = ownpage + "topline=";
startRender(writer);
if (topline > 1)
renderBackButton(writer, topline);
renderExpandAllButton(writer, topline);
renderCloseAllButton(writer, topline);
if (topline + windowSize < nodes)
renderForwardButton(writer, topline);
writer.write("<br><br>\n");
writer.write("<span id='" + name + "'>\n");
List children = node.getChildren();
int lineno = 0;
for (int ix = 0; ix < children.size(); ix++)
lineno = writeNode((TopicTreeNode) children.get(ix), topline, writer, 0,
lineno, false);
writer.write("</span>");
writer.write("<br>");
if (topline > 1)
renderBackButton(writer, topline);
renderExpandAllButton(writer, topline);
renderCloseAllButton(writer, topline);
if (topline + windowSize < nodes)
renderForwardButton(writer, topline);
endRender(writer);
writer.write("<script>\n");
writer.write(" pNodes_" + name + " = new Array(");
Iterator iter = pNodes.iterator();
while (iter.hasNext()) {
writer.write('"');
String v = (String)iter.next();
writer.write(v);
writer.write('"');
if (iter.hasNext()) writer.write(',');
}
writer.write(");\n");
writer.write("</script>\n");
}
private int writeNode(TopicTreeNode node, int topline, Writer writer,
int level, int lineno, boolean indoc) throws IOException {
boolean nextIndoc = indoc;
String id = (String) node.getAttribute("id");
String action = (String) node.getAttribute("action");
if (action != null && action.equals("close"))
action = "open";
boolean isopen = action != null && action.equals("open");
lineno++;
if (lineno >= topline && lineno <= topline + windowSize) {
if (action == null)
writer.write("<img border=0 src=" + imageurl + "spacer.gif width="
+ (level * 30) + " height=5>" + "<img border=0 src=" + imageurl
+ "boxed.gif>");
else
renderNodeButton(topline, level, isopen ? OPEN : CLOSE, id, writer);
writer.write("<a name=" + id + "></a>");
renderNode(node, writer);
writer.write("<br>\n");
}
if (lineno < topline + windowSize) {
List children = node.getChildren();
if (!children.isEmpty()) {
pNodes.add(id);
writer.write("<span class=pnode id=" + getQualifiedId(id) + "span style=\"display: " +
(isopen ? "none" : "inline") + "\">");
}
for (int ix = 0; ix < children.size(); ix++)
lineno = writeNode((TopicTreeNode) children.get(ix), topline, writer,
level + 1, lineno, nextIndoc);
if (!children.isEmpty())
writer.write("</span>");
}
return lineno;
}
// --- Helpers
private int getAction(Map parameters) {
String action = get(parameters, "todo");
if (action == null)
action = "close";
if (action.equals("open"))
return OPEN;
else if (action.equals("close"))
return CLOSE;
else if (action.equals("expandall"))
return EXPAND_ALL;
else if (action.equals("closeall"))
return CLOSE_ALL;
else
return -1;
}
// --- Utilities
protected TopicIF getTopic(String id) {
return (TopicIF) topicmap.getObjectById(id);
}
protected String getId(TopicIF topic) {
return topic.getObjectId();
}
protected String getQualifiedId(String id) {
return name + "_" + id;
}
protected String list(Set nodes) {
StringBuilder buf = new StringBuilder();
Iterator it = nodes.iterator();
while (it.hasNext())
buf.append("," + getId((TopicIF) it.next()));
return buf.toString();
}
private String get(Map parameters, String name) {
// the servlets 2.3 spec says the Map should be String -> String[],
// but Oracle 9iAS has String -> String, so we need to code around
// that
Object value = parameters.get(name);
if (value instanceof String) // then your app server is broken
return (String) value;
// else, we continue like normal
String[] values = (String[]) value;
if (values == null)
return null;
return values[0];
}
// NOTE: we *don't* count the root node (it's a false convenience node)
private int countNodes(TopicTreeNode node) {
List children = node.getChildren();
int size = children.size();
int count = 0;
for (int ix = 0; ix < size; ix++)
count += countNodes((TopicTreeNode) children.get(ix));
return count + size;
}
public String toString(TopicIF topic) {
try {
if (topic == null)
return "null";
else
return Stringificator.toString(context, topic);
} catch (NavigatorRuntimeException e) {
throw new OntopiaRuntimeException(e);
}
}
// --- Extension interface
/**
* PUBLIC: This method renders the tree node, including its link, but
* <em>not</em> the button in front of the node. Intended to be overridden
* by applications wanting to control rendering of nodes in detail.
*/
protected void renderNode(TopicTreeNode node, Writer out) throws IOException {
TopicIF topic = node.getTopic();
if (topic != null) {
out.write("<a href=\"" + makeNodeUrl(node) + "\"");
if (nodeFrame != null)
out.write(" target=\"" + nodeFrame + "\"");
if (debug) {
Object[] data = (Object[])node.getAttribute("data");
if (data == null)
out.write(" title=\"*No data*\"");
else if (data.length == 1)
out.write(" title=\"" + data[0] + "\"");
else
out.write(" title=\"" + java.util.Arrays.asList(data) + "\"");
}
out.write(">");
}
out.write(toString(topic));
if (topic != null)
out.write("</a>");
}
/**
* PUBLIC: Renders the +/- button in front of the node. Intended to be
* overridden.
*/
protected void renderNodeButton(int topline, int level, int action,
String id, Writer out) throws IOException {
String image = "expand";
if (action == CLOSE)
image = "collapse";
out.write("<a onclick=\"open_close_" + name + "('" + id + "')\">" + "<img border=0 src="
+ imageurl + "spacer.gif width=" + (level * 30) + " height=5>"
+ "<img border=0 id=" + getQualifiedId(id) + " src=" + imageurl + image + ".gif></a>");
}
/**
* PUBLIC: Renders the back button at the top/bottom of the form.
*/
protected void renderBackButton(Writer out, int topline) throws IOException {
out.write("<a href=\"" + staticurl + (topline - windowSize)
+ "\" title='Show previous page'><img border=0 src=" + imageurl
+ "nav_prev.gif></a> ");
}
/**
* PUBLIC: Renders the expand all button at the top/bottom of the form.
*/
protected void renderExpandAllButton(Writer out, int topline)
throws IOException {
out.write("<a onclick=\"expand_all_" + name + "()\" title='Expand all nodes'><img border=0 src=" + imageurl + "expand_all.gif></a> ");
}
/**
* PUBLIC: Renders the close all button at the top/bottom of the form.
*/
protected void renderCloseAllButton(Writer out, int topline)
throws IOException {
out.write("<a onclick=\"close_all_" + name + "()\" title='Collapse all nodes'><img border=0 src=" + imageurl + "collapse_all.gif></a> ");
}
/**
* PUBLIC: Renders the close all button at the top/bottom of the form.
*/
protected void renderForwardButton(Writer out, int topline)
throws IOException {
out.write("<a href=\"" + staticurl + (topline + windowSize)
+ "\" title='Show next page'><img border=0 src=" + imageurl
+ "nav_next.gif></a>");
}
/**
* PUBLIC: Called before rendering of the tree begins.
*/
protected void startRender(Writer out) throws IOException {}
/**
* PUBLIC: Called after the tree has been rendered.
*/
protected void endRender(Writer out) throws IOException {}
/**
* PUBLIC: Produces the URL to the given node.
*/
protected String makeNodeUrl(TopicTreeNode node) {
return nodepage + "id=" + getId(node.getTopic());
}
}