/* * #! * 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.servlets; import java.io.IOException; import java.io.Reader; import java.io.Writer; import java.util.Iterator; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.Map; import java.util.HashMap; import javax.servlet.jsp.PageContext; import javax.servlet.http.HttpServletRequest; import net.ontopia.utils.StringUtils; import net.ontopia.utils.OntopiaRuntimeException; import net.ontopia.infoset.core.LocatorIF; import net.ontopia.topicmaps.core.TMObjectIF; import net.ontopia.topicmaps.core.TopicIF; import net.ontopia.topicmaps.core.TopicMapIF; import net.ontopia.topicmaps.utils.TopicTreeNode; import net.ontopia.topicmaps.query.core.ParsedQueryIF; import net.ontopia.topicmaps.query.utils.QueryUtils; import net.ontopia.topicmaps.query.core.QueryProcessorIF; import net.ontopia.topicmaps.query.core.InvalidQueryException; import net.ontopia.topicmaps.query.core.QueryResultIF; import net.ontopia.topicmaps.nav2.core.NavigatorPageIF; import net.ontopia.topicmaps.nav2.core.NavigatorRuntimeException; import net.ontopia.topicmaps.nav2.utils.FrameworkUtils; import net.ontopia.topicmaps.nav2.utils.Stringificator; /** * EXPERIMENTAL: This class is highly experimental. We recommend that * you not use this class. If you do, expect it to break in the next * version. * * @since 2.0 * @deprecated Use the version in nav2.utils. This version will be * removed soon. */ public class TreeWidget { 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 Set openNodes; protected String imageurl; protected int windowSize; private NavigatorPageIF context; private String name; private ParsedQueryIF query; private TopicMapIF topicmap; private LocatorIF base; private Map map; private String topquery; private String ownpage; private String querystr; // string repr of query, parsed into 'query' private String nodeFrame; private boolean addAnchor; // --- External interface /** * Sets up the widget ready for use. * @param topicmap The topic map being displayed. * @param query A tolog query that given a node generates its * children. Use the %parent% parameter to reference the parent node * in the query. Make sure the query produces a 1-column result. * @param topquery A tolog query that generates the list of top * nodes. Make sure the query produces a 1-column result. * @param ownpage The URI 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 URI of of the page that shows the nodes. */ public TreeWidget(TopicMapIF topicmap, String query, String topquery, String ownpage, String nodepage) throws InvalidQueryException { this.topicmap = topicmap; this.topquery = topquery; this.ownpage = ownpage; this.nodepage = nodepage; this.querystr = query; this.name = "ONTOPIA-WIDGET-ATTRIBUTE"; processor = QueryUtils.getQueryProcessor(topicmap); base = topicmap.getStore().getBaseAddress(); map = new HashMap(); imageurl = ""; addAnchor = true; windowSize = WINDOW_SIZE; } /** * EXPERIMENTAL: The name of the session key in which the set of * open nodes is stored. * * @since 2.0.4 */ public void setWidgetName(String name) { this.name = name; } /** * EXPERIMENTAL: The URL at which the graphics used by the widget * are found. * * @since 2.0.4 */ public void setImageUrl(String imageurl) { this.imageurl = imageurl; } /** * EXPERIMENTAL: FIXME. * * @since 2.0.4 */ public void setAddAnchor(boolean addAnchor) { this.addAnchor = addAnchor; } /** * EXPERIMENTAL: FIXME * * @since 2.0.4 */ public void setWindowSize(int windowSize) { this.windowSize = windowSize; } /** * EXPERIMENTAL: The name of the HTML frame in which to open links * to nodes. * * @since 2.0.3 */ public void setNodeFrame(String nodeFrame) { this.nodeFrame = nodeFrame; } /** * EXPERIMENTAL: We <b>really</b> don't recommend that you use this * method. */ public void loadRules(Reader rulereader) throws IOException, InvalidQueryException { processor.load(rulereader); } /** * EXPERIMENTAL: Runs the widget, producing the output. */ public void run(PageContext ctxt, Writer writer) throws IOException, InvalidQueryException, NavigatorRuntimeException { context = FrameworkUtils.getContextTag(ctxt); HttpServletRequest request = (HttpServletRequest) ctxt.getRequest(); Map parameters = request.getParameterMap(); // check that query has been parsed if (query == null) query = processor.parse(querystr); // get current node TopicIF current = null; if (parameters.containsKey("current")) current = getTopic(get(parameters, "current")); int action = getAction(parameters); if (action == EXPAND_ALL) openNodes = new UniversalSet(); else if (action == CLOSE_ALL) openNodes = new HashSet(); else { openNodes = getOpenNodes(request); if (action == OPEN) openNodes.add(current); else if (action == CLOSE) openNodes.remove(current); } 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); } setOpenNodes(request, openNodes); } 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); QueryResultIF result = processor.execute(topquery); while (result.next()) { TopicIF topic = (TopicIF) result.getValue(0); TopicTreeNode group = new TopicTreeNode(topic); group.setParent(root); group.setAttribute("id", getId(topic)); if (openNodes.contains(topic)) { process(group); group.setAttribute("action", "close"); openNodes.add(topic); } else { // check if there are children QueryResultIF leaves = getChildren(topic); if (leaves.next()) group.setAttribute("action", "open"); else group.setAttribute("action", null); } } return root; } 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); if (openNodes.contains(childtopic)) { process(child); if (!child.getChildren().isEmpty()) child.setAttribute("action", "close"); openNodes.add(childtopic); } else { // this one is not open; need to check if it has children QueryResultIF leaves = getChildren(childtopic); if (leaves.next()) child.setAttribute("action", "open"); else child.setAttribute("action", null); } } } private void writeHTML(TopicTreeNode node, int topline, Writer writer) throws IOException { 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"); 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("<br>"); if (topline > 1) renderBackButton(writer, topline); renderExpandAllButton(writer, topline); renderCloseAllButton(writer, topline); if (topline + windowSize < nodes) renderForwardButton(writer, topline); endRender(writer); } private int writeNode(TopicTreeNode node, int topline, Writer writer, int level, int lineno, boolean indoc) throws IOException { boolean nextIndoc = indoc; lineno++; if (lineno >= topline && lineno <= topline + windowSize) { String id = (String) node.getAttribute("id"); String action = (String) node.getAttribute("action"); 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, action.equals("open") ? 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(); for (int ix = 0; ix < children.size(); ix++) lineno = writeNode((TopicTreeNode) children.get(ix), topline, writer, level+1, lineno, nextIndoc); } 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; } private Set getOpenNodes(HttpServletRequest request) { String opennodes = (String) request.getSession().getAttribute(name); if (opennodes == null) opennodes = ""; return makeSet(StringUtils.split(opennodes, ",")); } private void setOpenNodes(HttpServletRequest request, Set openNodes) { request.getSession().setAttribute(name, list(openNodes)); } // --- Utilities private Set makeSet(String[] open) { Set nodes = new HashSet(open.length * 2); for (int ix = 0; ix < open.length; ix++) { if (open[ix].equals("")) continue; TMObjectIF object = topicmap.getObjectById(open[ix]); // if the ID list in the session is out of date, because the // topic map was reloaded or changed in the meantime, we may // get non-existent topics or non-topics back here. if this // happens we assume we have out-of-date info and stop if (object == null || !(object instanceof TopicIF)) return new HashSet(); nodes.add(object); } return nodes; } protected TopicIF getTopic(String id) { return (TopicIF) topicmap.getObjectById(id); } protected String getId(TopicIF topic) { return topic.getObjectId(); } 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) { String[] values = (String[]) parameters.get(name); 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 { return Stringificator.toString(context, topic); } catch (NavigatorRuntimeException e) { throw new OntopiaRuntimeException(e); } } // --- Extension interface /** * EXPERIMENTAL: 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=\"" + nodepage + "id=" + getId(topic) + "\""); if (nodeFrame != null) out.write(" target=\"" + nodeFrame + "\""); out.write(">"); } out.write(toString(topic)); if (topic != null) out.write("</a>"); } /** * EXPERIMENTAL: Renders the +/- button in front of the node. */ 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 href=\"" + staticurl + topline + "&todo=" + (action == OPEN ? "open" : "close") + "¤t=" + id + (addAnchor ? ("#" + id) : "" ) + "\">" + "<img border=0 src=" + imageurl + "spacer.gif width=" + (level * 30) + " height=5>" + "<img border=0 src=" + imageurl + image + ".gif></a>"); } /** * EXPERIMENTAL: 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> "); } /** * EXPERIMENTAL: Renders the expand all button at the top/bottom of * the form. */ protected void renderExpandAllButton(Writer out, int topline) throws IOException { out.write("<a href=\"" + ownpage + "topline=" + topline + "&todo=expandall\" title='Expand all nodes'><img border=0 src=" + imageurl + "expand_all.gif></a> "); } /** * EXPERIMENTAL: Renders the close all button at the top/bottom of * the form. */ protected void renderCloseAllButton(Writer out, int topline) throws IOException { out.write("<a href=\"" + ownpage + "topline=0&todo=closeall\" title='Collapse all nodes'><img border=0 src=" + imageurl + "collapse_all.gif></a> "); } /** * EXPERIMENTAL: 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>"); } protected void startRender(Writer out) throws IOException { } protected void endRender(Writer out) throws IOException { } // --- UniversalSet class // used when the "expand all" button is pressed. claims that all // topics are open, and since open topics are always added to the // set it also records which ones are open. this means that during // the tree traversal phase all nodes are included and added to the // set, so that when the staticurl is produced later all nodes are // included in the url. (this is necessary to ensure that the close // node buttons work as expected.) // this class is a hack, and therefore defined as an internal class. class UniversalSet extends HashSet { public boolean contains(Object object) { return true; } } }