0001: /*
0002: * argun 1.0
0003: * Web 2.0 delivery framework
0004: * Copyright (C) 2007 Hammurapi Group
0005: *
0006: * This program is free software; you can redistribute it and/or
0007: * modify it under the terms of the GNU Lesser General Public
0008: * License as published by the Free Software Foundation; either
0009: * version 2 of the License, or (at your option) any later version.
0010: *
0011: * This program is distributed in the hope that it will be useful,
0012: * but WITHOUT ANY WARRANTY; without even the implied warranty of
0013: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
0014: * Lesser General Public License for more details.
0015: *
0016: * You should have received a copy of the GNU Lesser General Public
0017: * License along with this library; if not, write to the Free Software
0018: * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
0019: *
0020: * URL: http://www.hammurapi.biz
0021: * e-Mail: support@hammurapi.biz
0022: */
0023: package biz.hammurapi.web.menu;
0024:
0025: import java.io.IOException;
0026: import java.io.StringReader;
0027: import java.io.Writer;
0028: import java.lang.ref.Reference;
0029: import java.lang.ref.SoftReference;
0030: import java.sql.SQLException;
0031: import java.util.ArrayList;
0032: import java.util.Collection;
0033: import java.util.HashMap;
0034: import java.util.HashSet;
0035: import java.util.Iterator;
0036: import java.util.List;
0037: import java.util.Map;
0038: import java.util.Set;
0039: import java.util.StringTokenizer;
0040: import java.util.TreeMap;
0041:
0042: import org.apache.log4j.Logger;
0043: import org.w3c.dom.Element;
0044:
0045: import antlr.Token;
0046: import antlr.TokenStreamException;
0047: import biz.hammurapi.config.Context;
0048: import biz.hammurapi.config.PropertyParser;
0049: import biz.hammurapi.util.Attributable;
0050: import biz.hammurapi.web.menu.sql.HelpTopic;
0051: import biz.hammurapi.web.menu.sql.MenuEngine;
0052: import biz.hammurapi.web.menu.sql.MenuHelpTopics;
0053: import biz.hammurapi.web.util.HTMLLexer;
0054: import biz.hammurapi.web.util.HTMLTokenTypes;
0055: import biz.hammurapi.xml.dom.AbstractDomObject;
0056: import biz.hammurapi.xml.dom.DomSerializable;
0057:
0058: /**
0059: * Helper class to form help topic tree.
0060: * @author Pavel Vlasov
0061: *
0062: */
0063: public class Help {
0064: private static final String TERM_URL_PREFIX = "term:";
0065:
0066: private static final String TOPIC_URL_PREFIX = "topic:";
0067:
0068: private static final String CT_URL = "URL";
0069:
0070: private static final String CT_ALIAS = "Alias";
0071:
0072: private static final String CT_CONTENT = "Content";
0073:
0074: private static final int MIN_TOOLTIP_LENGTH = 100;
0075:
0076: private static final String HELP_ATTRIBUTE = "help";
0077:
0078: private static final Logger logger = Logger.getLogger(Help.class);
0079:
0080: private static final String MENU_ENGINE_PATH = "global:db/MenuEngine";
0081:
0082: private static final String TOPIC_TRANSCLUSION_CLOSE = "}}}";
0083:
0084: private static final String TOPIC_TRANSCLUSION_OPEN = "{{{";
0085:
0086: private static final String TERM_TRANSCLUSION_CLOSE = "}}";
0087:
0088: private static final String TERM_TRANSCLUSION_OPEN = "{{";
0089:
0090: private static final String TOPIC_CLOSE = "]]]";
0091:
0092: private static final String TOPIC_OPEN = "[[[";
0093:
0094: private static final String TERM_CLOSE = "]]";
0095:
0096: private static final String TERM_OPEN = "[[";
0097:
0098: private static final String HELP_TERM = "HelpTerm";
0099:
0100: private static final String SAME_PAGE_HELP_TERM = "SamePageHelpTerm";
0101:
0102: private static final String CONTEXT_TERM_PREFIX = "context:";
0103:
0104: private static int counter;
0105:
0106: public static final int DEFAULT_MAX_TOOLTIP_LENGTH = 1000;
0107: public static final String DEFAULT_TAIL = "...";
0108: private static final String TOOLTIP_END_MARKER = "tooltip-end";
0109: static final String ELLIPSIS = " ...";
0110:
0111: private String name;
0112: private Map children = new TreeMap();
0113: private Menu owner;
0114: private Help parent;
0115: private String id;
0116: private MenuHelpTopics menuHelpTopics;
0117: private Reference words; // Words map for search.
0118: private Map content = new HashMap(); // page content.
0119: private Reference tooltip; // content without header truncated if needed.
0120:
0121: /**
0122: * URL loaded from mount point.
0123: */
0124: private String url;
0125:
0126: Help(String name, Help parent) {
0127: this .name = name;
0128: this .parent = parent;
0129: id = "V" + (++counter);
0130: this .owner = parent.owner;
0131: }
0132:
0133: public Help(Menu owner) {
0134: this .owner = owner;
0135: id = "M" + owner.getId();
0136: this .name = owner.getName();
0137: }
0138:
0139: public String getId() {
0140: return id;
0141: }
0142:
0143: public String getName() {
0144: return isSection() ? name.substring(1) : name;
0145: }
0146:
0147: public String getTitle() {
0148: if (menuHelpTopics != null) {
0149: return menuHelpTopics.getTitle();
0150: }
0151:
0152: if (parent == null) {
0153: return owner.getTitle();
0154: }
0155:
0156: return null;
0157: }
0158:
0159: public boolean isSection() {
0160: if (parent != null && parent.isSection()) {
0161: return true;
0162: }
0163:
0164: return name != null && name.startsWith("#");
0165: }
0166:
0167: /**
0168: * @param name - Name "tail"
0169: * @param mht
0170: * @param isSection True if name separated from parent by #, which means section.
0171: */
0172: public void add(String name, MenuHelpTopics mht) {
0173: if (name == null) {
0174: if (menuHelpTopics == null) {
0175: menuHelpTopics = mht;
0176: ((Attributable) mht).setAttribute(HELP_ATTRIBUTE, this );
0177: id = "T" + mht.getId();
0178: } else {
0179: throw new IllegalStateException(
0180: "Duplicate topic for the same help tree node: "
0181: + mht.getName());
0182: }
0183: } else {
0184: //Hash cannot be before slash.
0185: int slashIdx = name.indexOf("/");
0186: while (slashIdx != -1 && name.indexOf("//") == slashIdx) {
0187: slashIdx = name.indexOf("/", slashIdx + 2);
0188: }
0189: if (slashIdx == -1) {
0190: int hashIdx = name.indexOf("#");
0191: while (hashIdx != -1 && name.indexOf("##") == hashIdx) {
0192: hashIdx = name.indexOf("#", hashIdx + 2);
0193: }
0194: if (hashIdx == 0) {
0195: hashIdx = name.indexOf("#", 1);
0196: while (hashIdx != -1
0197: && name.indexOf("##") == hashIdx) {
0198: hashIdx = name.indexOf("#", hashIdx + 2);
0199: }
0200: }
0201: if (hashIdx == -1) {
0202: Help child = getChild(name);
0203: child.add(null, mht);
0204: } else {
0205: Help child = getChild(unescape(name.substring(0,
0206: hashIdx)));
0207: child.add(hashIdx == name.length() - 1 ? null
0208: : name.substring(hashIdx), mht);
0209: }
0210: } else {
0211: String nameTail = slashIdx == name.length() - 1 ? null
0212: : name.substring(slashIdx + 1);
0213: Help child = getChild(unescape(name.substring(0,
0214: slashIdx)));
0215: child.add(nameTail, mht);
0216: }
0217: }
0218: }
0219:
0220: /**
0221: * Replaces ## with # and // with /
0222: * @param str
0223: * @return
0224: */
0225: private static String unescape(String str) {
0226: StringBuffer ret = new StringBuffer();
0227: int idx;
0228: do {
0229: idx = str.indexOf("##");
0230: idx = idx == -1 ? str.indexOf("//") : Math.min(idx, str
0231: .indexOf("//"));
0232: if (idx != -1) {
0233: ret.append(str.substring(0, idx + 1));
0234: str = str.substring(idx + 2);
0235: }
0236: } while (idx != -1);
0237:
0238: ret.append(str);
0239: return ret.toString();
0240: }
0241:
0242: private Help getChild(String name) {
0243: Help child = (Help) children.get(name);
0244: if (child == null) {
0245: child = new Help(name, this );
0246: children.put(name, child);
0247: }
0248: return child;
0249: }
0250:
0251: /**
0252: * Helper method to construct <Hx> elements.
0253: * @param level
0254: * @return
0255: */
0256: private static int headLevel(int level) {
0257: return Math.min(level, 6);
0258: }
0259:
0260: /**
0261: *
0262: * @param level Nesting level. Shall be 1 for the page root and increase by one for eash subsection nesting.
0263: * @param engine
0264: * @return
0265: * @throws SQLException
0266: */
0267: public synchronized String getContent(int level, Context context,
0268: boolean withHeader, boolean withChildren, boolean isTooltip)
0269: throws SQLException {
0270: Collection key = new ArrayList();
0271: key.add(new Integer(level));
0272: key.add(withHeader ? Boolean.TRUE : Boolean.FALSE);
0273: key.add(withChildren ? Boolean.TRUE : Boolean.FALSE);
0274: key.add(isTooltip ? Boolean.TRUE : Boolean.FALSE);
0275: Reference ref = (Reference) content.get(key);
0276: String ret = (String) (ref == null ? null : ref.get());
0277: if (ret != null) {
0278: return ret;
0279: }
0280:
0281: Set transclusionSet = new HashSet();
0282: HeadNode head = getParsedContent(level, context, withHeader,
0283: withChildren, isTooltip, transclusionSet);
0284: String parsed = head.getText(context, transclusionSet,
0285: isTooltip);
0286: ref = new SoftReference(parsed);
0287: content.put(key, ref);
0288: return parsed;
0289: }
0290:
0291: private synchronized HeadNode getParsedContent(int level,
0292: Context context, boolean withHeader, boolean withChildren,
0293: boolean isTooltip, Set transclusionSet) throws SQLException {
0294: HeadNode head = new HeadNode();
0295: if (transclusionSet.add(getId())) {
0296: StringBuffer buf = new StringBuffer();
0297: // Cases: Menu root (main), with subsections, without.
0298: if (menuHelpTopics != null) {
0299: MenuEngine engine = (MenuEngine) context
0300: .get(MENU_ENGINE_PATH);
0301: if (engine == null) {
0302: throw new SQLException("Menu engine not found");
0303: }
0304: if (withHeader) {
0305: if (getName() != null) {
0306: // System.out.println(getName());
0307: buf.append("<a name=\"T"
0308: + menuHelpTopics.getId() + "\"><H"
0309: + headLevel(level) + ">" + getName()
0310: + "</H" + headLevel(level) + "></a>\n");
0311: } else if (parent == null) {
0312: if (owner.getName() != null) {
0313: buf.append("<H" + headLevel(level) + ">"
0314: + owner.getName() + "</H"
0315: + headLevel(level) + ">\n");
0316: }
0317: if (owner.getDescription() != null) {
0318: buf.append("<I>" + owner.getDescription()
0319: + "</I><P/>");
0320: }
0321: }
0322: }
0323: if (CT_CONTENT.equals(menuHelpTopics.getType())) {
0324: HelpTopic topic = engine
0325: .getHelpTopic(menuHelpTopics.getId());
0326: buf.append(topic.getContent());
0327: } else if (CT_ALIAS.equals(menuHelpTopics.getType())) {
0328: buf
0329: .append("<SPAN style=\"float:right; padding:2px\">Aliased from "
0330: + getName() + "</SPAN>");
0331: if (menuHelpTopics.getTopicUrl().startsWith(
0332: TOPIC_URL_PREFIX)) {
0333: buf.append("{{{");
0334: buf.append(menuHelpTopics.getTopicUrl()
0335: .substring(TOPIC_URL_PREFIX.length()));
0336: buf.append("}}}");
0337: } else if (menuHelpTopics.getTopicUrl().startsWith(
0338: TERM_URL_PREFIX)) {
0339: buf.append("{{");
0340: buf.append(menuHelpTopics.getTopicUrl()
0341: .substring(TERM_URL_PREFIX.length()));
0342: buf.append("}}");
0343: } else {
0344: buf
0345: .append("<I>Invalid alias: "
0346: + menuHelpTopics.getTopicUrl()
0347: + "</I>");
0348: }
0349: } else if (CT_URL.equals(menuHelpTopics.getType())) {
0350: url = menuHelpTopics.getTopicUrl();
0351: urlLink(context, buf);
0352: } else {
0353: urlLink(context, buf);
0354: }
0355: } else if (parent == null) {
0356: if (withHeader) {
0357: if (owner.getName() != null) {
0358: buf.append("<H" + headLevel(level) + ">"
0359: + owner.getName() + "</H"
0360: + headLevel(level) + ">\n");
0361: }
0362: if (owner.getDescription() != null) {
0363: buf.append("<I>" + owner.getDescription()
0364: + "</I><P/>");
0365: }
0366: }
0367: toc(buf, context);
0368: } else if (getName() != null) {
0369: buf.append("<a name=\"" + getId() + "\"><H"
0370: + headLevel(level) + ">" + getName() + "</H"
0371: + headLevel(level) + "></a>\n");
0372: urlLink(context, buf);
0373: toc(buf, context);
0374: }
0375:
0376: // Transclusion and references.
0377: // Break string into nodes - text, {{{, {{, [[[, [[
0378: parse(buf.toString(), head, level);
0379:
0380: if (withChildren) {
0381: Iterator it = children.values().iterator();
0382: while (it.hasNext()) {
0383: Help child = (Help) it.next();
0384: if (child.isSection()) {
0385: head.append(child.getParsedContent(level + 1,
0386: context, true, true, isTooltip,
0387: transclusionSet));
0388: }
0389: }
0390: }
0391: } else {
0392: head
0393: .setNext(new TextNode(
0394: "<I>{{{Transclusion error: circular reference}}}</I>"));
0395: }
0396:
0397: transclusionSet.remove(getId()); // Allow to transclude the same content sequentially, but not hierarchically
0398: return head;
0399: }
0400:
0401: private void urlLink(Context context, StringBuffer buf) {
0402: PropertyParser pp = new PropertyParser(context, false);
0403: if (url != null) {
0404: String realUrl = pp.parse(url);
0405: buf.append("See <a href=\"");
0406: buf.append(realUrl);
0407: buf.append("\">");
0408: buf.append(realUrl);
0409: buf.append("</a><P/>");
0410: }
0411: }
0412:
0413: /**
0414: * Table of content for help nodes without own content.
0415: * @param buf
0416: * @param context
0417: */
0418: private void toc(StringBuffer buf, Context context) {
0419: StringBuffer tbuf = new StringBuffer();
0420: Iterator it = children.values().iterator();
0421: while (it.hasNext()) {
0422: Help child = (Help) it.next();
0423: if (!child.isSection()) {
0424: tbuf.append("<LI> <a href=\"helpPage.html?id=");
0425: tbuf.append(child.getId());
0426: tbuf.append("\">");
0427: tbuf.append(Menu.escapeHtml(child.getName()));
0428: tbuf.append("</a>");
0429: String childTitle = child.getTitle();
0430: if (childTitle != null && child.getTitle().length() > 0) {
0431: tbuf.append(" - ");
0432: tbuf.append(Menu.escapeHtml(childTitle));
0433: }
0434: tbuf.append("</LI>");
0435: }
0436: }
0437:
0438: if (tbuf.length() > 0) {
0439: buf.append("<B>Topics</B><P/><UL>");
0440: buf.append(tbuf);
0441: buf.append("</UL><P/>");
0442: }
0443:
0444: if (parent == null) {
0445: tbuf = new StringBuffer();
0446: it = owner.getChildren().iterator();
0447: while (it.hasNext()) {
0448: Menu mc = (Menu) it.next();
0449: if (mc.hasHelp() || !mc.getChildren().isEmpty()) {
0450: Help child = mc.getHelp(context);
0451: tbuf.append("<LI> <a href=\"helpPage.html?id=");
0452: tbuf.append(child.getId());
0453: tbuf.append("\">");
0454: String childName = child.getName();
0455: tbuf.append(Menu.escapeHtml(childName));
0456: tbuf.append("</a>");
0457: String childTitle = child.getTitle();
0458: if (childTitle != null && childTitle.length() > 0) {
0459: tbuf.append(" - ");
0460: tbuf.append(Menu.escapeHtml(childTitle));
0461: }
0462: tbuf.append("</LI>");
0463: }
0464: }
0465:
0466: if (tbuf.length() > 0) {
0467: buf.append("<B>Subitems</B><P/><UL>");
0468: buf.append(tbuf);
0469: buf.append("</UL>");
0470: }
0471:
0472: }
0473: }
0474:
0475: /**
0476: * Writes content of self, subtopics and menu children to writer.
0477: * @param context
0478: * @param writer
0479: * @throws IOException
0480: * @throws SQLException
0481: */
0482: public void writeSinglePageContent(Context context, Writer writer)
0483: throws IOException, SQLException {
0484: writer.write(getContent(1, context, true, true, false));
0485: Iterator it = children.values().iterator();
0486: while (it.hasNext()) {
0487: Help child = (Help) it.next();
0488: if (!child.isSection()) {
0489: child.writeSinglePageContent(context, writer);
0490: }
0491: }
0492:
0493: if (parent == null) {
0494: writer
0495: .write("<div style=\"PAGE-BREAK-AFTER: always\"><span style=\"DISPLAY: none\"> </span></div>");
0496: it = owner.getChildren().iterator();
0497: while (it.hasNext()) {
0498: Menu child = (Menu) it.next();
0499: if (child.hasHelp() || !child.getChildren().isEmpty()) {
0500: child.getHelp(context).writeSinglePageContent(
0501: context, writer);
0502: }
0503: }
0504: }
0505: }
0506:
0507: private void parse(String input, HelpNode head, int level) {
0508: // 1. Break into {{{...|...}}}
0509: HelpNode lastNode = head;
0510: int idx = input.indexOf(TOPIC_TRANSCLUSION_OPEN);
0511: while (idx != -1) {
0512: int cidx = input.indexOf(TOPIC_TRANSCLUSION_CLOSE, idx);
0513: if (cidx == -1) { // Wrong syntax
0514: break;
0515: } else {
0516: int pidx = input.indexOf("|");
0517: if (pidx == -1 || pidx > cidx) { // Wrong syntax - output {{{...}}} as is.
0518: TextNode tn = new TextNode(input.substring(idx,
0519: cidx + 3));
0520: lastNode.setNext(tn);
0521: lastNode = tn;
0522: input = input.substring(cidx + 3);
0523: idx = input.indexOf(TOPIC_TRANSCLUSION_OPEN);
0524: } else { // Correct syntax
0525: if (idx != 0) {
0526: TextNode tn = new TextNode(input.substring(0,
0527: idx));
0528: lastNode.setNext(tn);
0529: lastNode = tn;
0530: }
0531: TopicTransclusionNode ttn = new TopicTransclusionNode(
0532: input.substring(idx + 3, pidx), input
0533: .substring(pidx + 1, cidx), level);
0534: lastNode.setNext(ttn);
0535: lastNode = ttn;
0536: input = input.substring(cidx + 3);
0537: idx = input.indexOf(TOPIC_TRANSCLUSION_OPEN);
0538: }
0539: }
0540: }
0541: TextNode tn = new TextNode(input);
0542: lastNode.setNext(tn);
0543: lastNode = tn;
0544:
0545: // 2. Go through text nodes and break them at {{...}}
0546: List textNodes = new ArrayList();
0547: for (HelpNode node = head; node != null; node = node.getNext()) {
0548: if (node instanceof TextNode) {
0549: textNodes.add(node);
0550: }
0551: }
0552:
0553: Iterator it = textNodes.iterator();
0554: while (it.hasNext()) {
0555: TextNode textNode = (TextNode) it.next();
0556: HelpNode before = textNode.getPrev();
0557: HelpNode after = textNode.getNext();
0558: String txt = textNode.getText(null, null, false);
0559: int tidx = txt.indexOf(TERM_TRANSCLUSION_OPEN);
0560: while (tidx != -1) {
0561: int cidx = txt.indexOf(TERM_TRANSCLUSION_CLOSE, tidx);
0562: if (cidx == -1) {
0563: break;
0564: }
0565: if (tidx > 0) {
0566: TextNode ttn = new TextNode(txt.substring(0, tidx));
0567: before.setNext(ttn);
0568: before = ttn;
0569: }
0570: TermTransclusionNode ttn = new TermTransclusionNode(txt
0571: .substring(tidx + 2, cidx), level);
0572: before.setNext(ttn);
0573: before = ttn;
0574: txt = txt.substring(cidx + 2);
0575: tidx = txt.indexOf(TERM_TRANSCLUSION_OPEN);
0576: }
0577: TextNode ttn = new TextNode(txt);
0578: before.setNext(ttn);
0579: ttn.setNext(after);
0580: }
0581:
0582: // 3. Go through text nodes and break them at [[[...|...]]
0583: textNodes = new ArrayList();
0584: for (HelpNode node = head; node != null; node = node.getNext()) {
0585: if (node instanceof TextNode) {
0586: textNodes.add(node);
0587: }
0588: }
0589:
0590: it = textNodes.iterator();
0591: while (it.hasNext()) {
0592: TextNode textNode = (TextNode) it.next();
0593: HelpNode before = textNode.getPrev();
0594: HelpNode after = textNode.getNext();
0595: String txt = textNode.getText(null, null, false);
0596: int tidx = txt.indexOf(TOPIC_OPEN);
0597: while (tidx != -1) {
0598: int cidx = txt.indexOf(TOPIC_CLOSE, tidx);
0599: if (cidx == -1) {
0600: break;
0601: }
0602: int pidx = txt.indexOf("|", tidx);
0603: if (pidx == -1 || pidx > cidx) {// Wrong syntax - output [[[...]]] as is.
0604: TextNode ttn = new TextNode(txt.substring(tidx,
0605: cidx + 3));
0606: before.setNext(ttn);
0607: before = ttn;
0608: txt = txt.substring(cidx + 3);
0609: idx = txt.indexOf(TOPIC_OPEN);
0610: } else {
0611: if (tidx > 0) {
0612: TextNode ttn = new TextNode(txt.substring(0,
0613: tidx));
0614: before.setNext(ttn);
0615: before = ttn;
0616: }
0617: String menuPath = txt.substring(tidx + 3, pidx);
0618: String topic = txt.substring(pidx + 1, cidx);
0619: int tpidx = topic.indexOf("|");
0620: TopicNode ttn = new TopicNode(menuPath,
0621: tpidx == -1 ? topic : topic.substring(0,
0622: tpidx), tpidx == -1 ? null : topic
0623: .substring(tpidx + 1));
0624: before.setNext(ttn);
0625: before = ttn;
0626: txt = txt.substring(cidx + 3);
0627: tidx = txt.indexOf(TOPIC_OPEN);
0628: }
0629: }
0630: TextNode ttn = new TextNode(txt);
0631: before.setNext(ttn);
0632: ttn.setNext(after);
0633: }
0634:
0635: // 4. Go through text nodes and break them at [[...]]
0636: textNodes = new ArrayList();
0637: for (HelpNode node = head; node != null; node = node.getNext()) {
0638: if (node instanceof TextNode) {
0639: textNodes.add(node);
0640: }
0641: }
0642:
0643: it = textNodes.iterator();
0644: while (it.hasNext()) {
0645: TextNode textNode = (TextNode) it.next();
0646: HelpNode before = textNode.getPrev();
0647: HelpNode after = textNode.getNext();
0648: String txt = textNode.getText(null, null, false);
0649: int tidx = txt.indexOf(TERM_OPEN);
0650: while (tidx != -1) {
0651: int cidx = txt.indexOf(TERM_CLOSE, tidx);
0652: if (cidx == -1) {
0653: break;
0654: }
0655: if (tidx > 0) {
0656: TextNode ttn = new TextNode(txt.substring(0, tidx));
0657: before.setNext(ttn);
0658: before = ttn;
0659: }
0660: String term = txt.substring(tidx + 2, cidx);
0661: int tpidx = term.indexOf("|");
0662: TermNode ttn = new TermNode(tpidx == -1 ? term : term
0663: .substring(0, tpidx), tpidx == -1 ? null : term
0664: .substring(tpidx + 1));
0665: before.setNext(ttn);
0666: before = ttn;
0667: txt = txt.substring(cidx + 2);
0668: tidx = txt.indexOf(TERM_OPEN);
0669: }
0670: TextNode ttn = new TextNode(txt);
0671: before.setNext(ttn);
0672: ttn.setNext(after);
0673: }
0674: }
0675:
0676: private abstract static class HelpNode {
0677: private HelpNode next;
0678: private HelpNode prev;
0679:
0680: public void setNext(HelpNode next) {
0681: if (this .next != next) {
0682: HelpNode oldNext = this .next;
0683: this .next = next;
0684: if (next != null && next.getPrev() != this ) {
0685: next._setPrev(this );
0686: }
0687: if (oldNext != null) {
0688: oldNext._setPrev(null);
0689: }
0690: }
0691: }
0692:
0693: private void _setNext(HelpNode next) {
0694: this .next = next;
0695: }
0696:
0697: public void setPrev(HelpNode prev) {
0698: if (this .prev != prev) {
0699: HelpNode oldPrev = this .prev;
0700: this .prev = prev;
0701: if (prev != null && prev.getNext() != this ) {
0702: prev._setNext(this );
0703: }
0704: if (oldPrev != null) {
0705: oldPrev._setNext(null);
0706: }
0707: }
0708: }
0709:
0710: private void _setPrev(HelpNode prev) {
0711: this .prev = prev;
0712: }
0713:
0714: public HelpNode getNext() {
0715: return next;
0716: }
0717:
0718: public HelpNode getPrev() {
0719: return prev;
0720: }
0721:
0722: public boolean isTooltipEnd() {
0723: return false;
0724: }
0725:
0726: public abstract String getText(Context context,
0727: Set transclusionSet, boolean isTooltip)
0728: throws SQLException;
0729: }
0730:
0731: /**
0732: * Head node, collects text from other nodes.
0733: * @author Pavel
0734: */
0735: private static class HeadNode extends HelpNode {
0736:
0737: public String getText(Context context, Set transclusionSet,
0738: boolean isTooltip) throws SQLException {
0739: StringBuffer ret = new StringBuffer();
0740: for (HelpNode next = getNext(); next != null; next = next
0741: .getNext()) {
0742: ret.append(next.getText(context, transclusionSet,
0743: isTooltip));
0744: if (isTooltip && next.isTooltipEnd()) {
0745: ret.append(TERM_TRANSCLUSION_OPEN
0746: + TOOLTIP_END_MARKER
0747: + TERM_TRANSCLUSION_CLOSE);
0748: break;
0749: }
0750: }
0751: return ret.toString();
0752: }
0753:
0754: public String toString() {
0755: StringBuffer ret = new StringBuffer();
0756: for (HelpNode next = getNext(); next != null; next = next
0757: .getNext()) {
0758: ret.append("\n");
0759: ret.append(next);
0760: }
0761: return ret.toString();
0762: }
0763:
0764: public void append(HeadNode otherNode) {
0765: if (otherNode.getNext() != null) {
0766: HelpNode lastNode = this ;
0767: while (lastNode.getNext() != null) {
0768: lastNode = lastNode.getNext();
0769: }
0770: lastNode.setNext(otherNode.getNext());
0771: }
0772: }
0773: }
0774:
0775: private static class TextNode extends HelpNode {
0776: private String text;
0777:
0778: public TextNode(String text) {
0779: this .text = text;
0780: }
0781:
0782: public String getText(Context context, Set transclusionSet,
0783: boolean isTooltip) {
0784: return text;
0785: }
0786:
0787: public String toString() {
0788: return "[Text] " + text;
0789: }
0790: }
0791:
0792: /**
0793: * Represents [[...]] constructs
0794: * @author Pavel
0795: *
0796: */
0797: private class TermNode extends HelpNode {
0798: private String term;
0799: private String label;
0800:
0801: public TermNode(String term, String label) {
0802: this .term = term;
0803: this .label = label;
0804: }
0805:
0806: public String getText(final Context context,
0807: Set transclusionSet, boolean isTooltip) {
0808: final Help trm = findTerm(this .term, new HashSet(), context);
0809: if (trm == null) {
0810: logger.info("Help term not found: " + this .term);
0811: return this .term;
0812: }
0813:
0814: Map ctx = new HashMap();
0815: ctx.put("id", trm.getId());
0816: if (trm.menuHelpTopics != null) {
0817: ctx.put("topicId", new Integer(trm.menuHelpTopics
0818: .getId()));
0819: }
0820: ctx.put("menuContext", context);
0821: ctx.put("help", trm);
0822: String lbl = label == null ? trm.getName() : label;
0823: ctx.put("label", lbl);
0824: ctx.put("context-path", context.get("context-path"));
0825: ctx.put("pageId", trm.getPageId());
0826: Object ret = owner.instantiateTemplate(getPageId().equals(
0827: trm.getPageId()) ? SAME_PAGE_HELP_TERM : HELP_TERM,
0828: ctx);
0829: return ret == null ? lbl : ret.toString();
0830: }
0831:
0832: public String toString() {
0833: return "[Term] " + term;
0834: }
0835: }
0836:
0837: /**
0838: * Represents [[[...|...]]] constructs
0839: * @author Pavel
0840: */
0841: private class TopicNode extends HelpNode {
0842: private String menuPath;
0843: private String topic;
0844: private String label;
0845:
0846: public TopicNode(String menuPath, String topic, String label) {
0847: this .menuPath = menuPath;
0848: this .topic = topic;
0849: this .label = label;
0850: }
0851:
0852: public String getText(final Context context,
0853: Set transclusionSet, boolean isTooltip) {
0854: int idx = topic.lastIndexOf("/");
0855: idx = idx == -1 ? topic.lastIndexOf("#") : topic
0856: .lastIndexOf("#", idx);
0857: String shortName = idx == -1 ? topic : topic
0858: .substring(idx + 1);
0859: Menu menu = owner.findItem(menuPath);
0860: if (menu == null) {
0861: logger.info("Menu not found: " + menuPath);
0862: return label == null ? shortName : label;
0863: }
0864:
0865: MenuHelpTopics mht = menu.getHelpTopic(topic, context);
0866: if (mht == null) {
0867: logger.info("Help topic not found: " + topic
0868: + " in menu " + menuPath);
0869: return label == null ? shortName : label; // Unexpanded.
0870: }
0871:
0872: final Help help = (Help) ((Attributable) mht)
0873: .getAttribute(HELP_ATTRIBUTE);
0874: if (help == null) {
0875: throw new IllegalStateException("Menu help topic "
0876: + mht.getId() + " without " + HELP_ATTRIBUTE
0877: + " attribute set");
0878: }
0879:
0880: Map ctx = new HashMap();
0881: ctx.put("id", help.getId());
0882: ctx.put("topicId", new Integer(mht.getId()));
0883: ctx.put("menuContext", context);
0884: ctx.put("context-path", context.get("context-path"));
0885: ctx.put("help", help);
0886: String lbl = label == null ? help.getName() : label;
0887: ctx.put("label", lbl);
0888: ctx.put("pageId", help.getPageId());
0889: Object ret = owner
0890: .instantiateTemplate(getPageId().equals(
0891: help.getPageId()) ? SAME_PAGE_HELP_TERM
0892: : HELP_TERM, ctx);
0893: return ret == null ? lbl : ret.toString();
0894: }
0895:
0896: public String toString() {
0897: return "[Topic] " + menuPath + ", " + topic;
0898: }
0899: }
0900:
0901: /**
0902: * Represents {{...}} constructs
0903: * @author Pavel
0904: *
0905: */
0906: private class TermTransclusionNode extends HelpNode {
0907: private String term;
0908: private int level;
0909:
0910: public TermTransclusionNode(String term, int level) {
0911: this .term = term;
0912: this .level = level;
0913: }
0914:
0915: public boolean isTooltipEnd() {
0916: return TOOLTIP_END_MARKER.equals(term);
0917: }
0918:
0919: public String getText(final Context context,
0920: Set transclusionSet, boolean isTooltip)
0921: throws SQLException {
0922: if (isTooltipEnd()) {
0923: return "";
0924: }
0925:
0926: // escape sequences
0927: if ("dlc".equals(term)) {
0928: return "{{";
0929: } else if ("drc".equals(term)) {
0930: return "}}";
0931: } else if ("dlb".equals(term)) {
0932: return "[[";
0933: } else if ("drb".equals(term)) {
0934: return "]]";
0935: } else if ("tlc".equals(term)) {
0936: return "{{{";
0937: } else if ("trc".equals(term)) {
0938: return "}}}";
0939: } else if ("tlb".equals(term)) {
0940: return "[[[";
0941: } else if ("trb".equals(term)) {
0942: return "]]]";
0943: } else if ("system.name".equals(term)) {
0944: return "Hammurapi Group Web Delivery Framework";
0945: }
0946:
0947: if ("context-path".equals(term)) {
0948: Object ret = context.get("context-path");
0949: return ret == null ? "" : ret.toString();
0950: }
0951:
0952: for (int i = 1; i <= 6; ++i) {
0953: if (("H" + i).equals(term)) {
0954: return "<H" + headLevel(level + i - 1) + ">";
0955: } else if (("/H" + i).equals(term)) {
0956: return "</H" + headLevel(level + i - 1) + ">";
0957: }
0958: }
0959:
0960: if (term.startsWith(CONTEXT_TERM_PREFIX)) {
0961: Object cte = context.get(term
0962: .substring(CONTEXT_TERM_PREFIX.length()));
0963: if (cte == null) {
0964: return "";
0965: }
0966: return cte.toString();
0967: }
0968:
0969: // normal term
0970: final Help trm = findTerm(this .term, new HashSet(), context);
0971: if (trm == null) {
0972: logger.info("Help term not found: " + this .term);
0973: return "{{" + this .term + "}}";
0974: }
0975:
0976: HeadNode termHead = trm.getParsedContent(level, context,
0977: false, true, false, transclusionSet);
0978: return termHead
0979: .getText(context, transclusionSet, isTooltip);
0980: }
0981:
0982: public String toString() {
0983: return "[Transclusion term] " + term;
0984: }
0985: }
0986:
0987: /**
0988: * Represents {{{...|...}}} constructs
0989: * @author Pavel
0990: *
0991: */
0992: private class TopicTransclusionNode extends HelpNode {
0993: private String menuPath;
0994: private String topic;
0995: private int level;
0996:
0997: public TopicTransclusionNode(String menuPath, String topic,
0998: int level) {
0999: this .menuPath = menuPath;
1000: this .topic = topic;
1001: this .level = level;
1002: }
1003:
1004: public String getText(Context context, Set transclusionSet,
1005: boolean isTooltip) throws SQLException {
1006: Menu menu = owner.findItem(menuPath);
1007: if (menu == null) {
1008: logger.info("Menu not found: " + menuPath);
1009: return "{{{" + menuPath + "|" + topic + "}}}"; // Unexpanded.
1010: }
1011:
1012: MenuHelpTopics mht = menu.getHelpTopic(topic, context);
1013: if (mht == null) {
1014: logger.info("Help topic not found: " + topic
1015: + " in menu " + menuPath);
1016: return "{{{" + menuPath + "|" + topic + "}}}"; // Unexpanded.
1017: }
1018:
1019: final Help help = (Help) ((Attributable) mht)
1020: .getAttribute(HELP_ATTRIBUTE);
1021: if (help == null) {
1022: throw new IllegalStateException("Menu help topic "
1023: + mht.getId() + " without " + HELP_ATTRIBUTE
1024: + " attribute set");
1025: }
1026:
1027: HeadNode helpHead = help.getParsedContent(level, context,
1028: false, true, isTooltip, transclusionSet);
1029: return helpHead
1030: .getText(context, transclusionSet, isTooltip);
1031: }
1032:
1033: public String toString() {
1034: return "[Transclusion topic] " + menuPath + ", " + topic;
1035: }
1036: }
1037:
1038: public String getTooltip(Context context) throws SQLException {
1039: String ret = (String) (tooltip == null ? null : tooltip.get());
1040: if (ret != null) {
1041: return ret;
1042: }
1043:
1044: ret = getContent(1, context, false, false, true);
1045: if (Menu.isBlank(ret)) {
1046: ret = "<I>No content</I>";
1047: tooltip = new SoftReference(ret);
1048: return ret;
1049: }
1050:
1051: //Number globalMaxTooltipLength = (Number) getGlobal(request, "db/max-tooltip-length");
1052: int maxTooltipLength = DEFAULT_MAX_TOOLTIP_LENGTH; //globalMaxTooltipLength==null ? DEFAULT_MAX_TOOLTIP_LENGTH : globalMaxTooltipLength.intValue();
1053:
1054: String ttem = TERM_TRANSCLUSION_OPEN + TOOLTIP_END_MARKER
1055: + TERM_TRANSCLUSION_CLOSE;
1056: if (ret.endsWith(ttem)) {
1057: ret = ret.substring(0, ret.length() - ttem.length())
1058: + ELLIPSIS;
1059: tooltip = new SoftReference(ret);
1060: return ret;
1061: }
1062:
1063: // int ltIdx = ret.lastIndexOf("<");
1064: // int gtIdx = ret.lastIndexOf(">");
1065: // if (ltIdx!=-1 && gtIdx<ltIdx) {
1066: // ret+=">";
1067: // }
1068:
1069: // Paragraph detection.
1070: Token lastPARA = null;
1071: List tokenChain = new ArrayList();
1072: HTMLLexer lexer = new HTMLLexer(new StringReader(ret));
1073: int textcounter = 0; // To protect against infinite loops.
1074: try {
1075: for (Token t = lexer.nextToken(); t != null
1076: && t.getType() != HTMLTokenTypes.CHTML
1077: && t.getType() != HTMLTokenTypes.EOF; t = lexer
1078: .nextToken()) {
1079: tokenChain.add(t);
1080: if (t.getText() != null
1081: && ((t.getType() == HTMLTokenTypes.UNDEFINED_TOKEN && t
1082: .getText().length() == 1) || t
1083: .getType() == HTMLTokenTypes.PCDATA)) {
1084: textcounter += t.getText().length();
1085: if (textcounter >= maxTooltipLength) {
1086: tokenChain.add(new Token(
1087: HTMLTokenTypes.UNDEFINED_TOKEN,
1088: ELLIPSIS));
1089: break;
1090: }
1091: }
1092:
1093: switch (t.getType()) {
1094: case HTMLTokenTypes.OPARA:
1095: case HTMLTokenTypes.CPARA:
1096: case HTMLTokenTypes.SAPARA:
1097: lastPARA = t;
1098: break;
1099: }
1100: }
1101: } catch (TokenStreamException e) {
1102: if (ret.length() > maxTooltipLength) {
1103: ret = ret.substring(0, maxTooltipLength) + ELLIPSIS;
1104: }
1105:
1106: tooltip = new SoftReference(ret);
1107: return ret;
1108: }
1109:
1110: if (lastPARA != null) {
1111: StringBuffer rsb = new StringBuffer();
1112: Iterator it = tokenChain.iterator();
1113: while (it.hasNext()) {
1114: Token t = (Token) it.next();
1115: if (t == lastPARA) {
1116: if (rsb.toString().trim().length() < MIN_TOOLTIP_LENGTH) {
1117: break;
1118: }
1119: rsb.append(ELLIPSIS);
1120: ret = rsb.toString();
1121: tooltip = new SoftReference(ret);
1122: return ret;
1123: }
1124: rsb.append(t.getText());
1125: }
1126: }
1127:
1128: StringBuffer rsb = new StringBuffer();
1129: Iterator it = tokenChain.iterator();
1130: while (it.hasNext()) {
1131: Token t = (Token) it.next();
1132: rsb.append(t.getText());
1133: }
1134: ret = rsb.toString();
1135: tooltip = new SoftReference(ret);
1136: return ret;
1137: }
1138:
1139: /**
1140: * For testing.
1141: * @param args
1142: */
1143: public static void main(String[] args) throws Exception {
1144: HeadNode head = new HeadNode();
1145: String str = "Hello {{{a|b}}} {{{db}}} my friend {{good}} and bad {{ [[[d|e]]], [[evil]] [[[alla]]] [[[]";
1146: System.out.println(str);
1147: new Help(null).parse(str, head, 1);
1148: System.out.println(head.getText(null, new HashSet(), false));
1149: }
1150:
1151: Help findHelp(String id) {
1152: if (id.equals(getId())) {
1153: return this ;
1154: }
1155:
1156: Iterator it = children.values().iterator();
1157: while (it.hasNext()) {
1158: Help child = (Help) it.next();
1159: Help ret = child.findHelp(id);
1160: if (ret != null) {
1161: return ret;
1162: }
1163: }
1164: return null;
1165: }
1166:
1167: Help findTerm(String term, Set idSet, Context context) {
1168: if (idSet.add(getId())) {
1169: if (term.equalsIgnoreCase(getName())) {
1170: return this ;
1171: }
1172:
1173: Iterator it = children.values().iterator();
1174: while (it.hasNext()) {
1175: Help child = (Help) it.next();
1176: Help ret = child.findTerm(term, idSet, context);
1177: if (ret != null) {
1178: return ret;
1179: }
1180: }
1181:
1182: if (parent == null) {
1183: return owner.findTerm(term, idSet, context);
1184: }
1185:
1186: return parent.findTerm(term, idSet, context);
1187: }
1188:
1189: return null;
1190: }
1191:
1192: /**
1193: * For sections returns page ID
1194: * @return
1195: */
1196: String getPageId() {
1197: return isSection() && parent != null ? parent.getPageId()
1198: : getId();
1199: }
1200:
1201: /**
1202: * Recursively outputs children.
1203: */
1204: public void toDom(Context context, Element holder) {
1205: toDomShallow(context, holder);
1206:
1207: Iterator it = children.values().iterator();
1208: while (it.hasNext()) {
1209: Help child = (Help) it.next();
1210: if (child.menuHelpTopics == null
1211: || !CT_ALIAS.equals(child.menuHelpTopics.getType())) {
1212: child.toDom(context, AbstractDomObject.addElement(
1213: holder, "help"));
1214: }
1215: }
1216:
1217: if (parent == null) {
1218: it = owner.getChildren().iterator();
1219: while (it.hasNext()) {
1220: Menu child = (Menu) it.next();
1221: if (child.hasHelp() || !child.getChildren().isEmpty()) {
1222: Element childElement = AbstractDomObject
1223: .addElement(holder, "menu");
1224: childElement.setAttribute("id", String
1225: .valueOf(child.getId()));
1226: childElement.setAttribute("name", child.getName());
1227: childElement.setAttribute("menu-type", child
1228: .getType());
1229: }
1230: }
1231: }
1232: }
1233:
1234: private void toDomShallow(Context context, Element holder) {
1235: if (parent == null) {
1236: holder.setAttribute("help-type", "menu");
1237: } else if (isSection()) {
1238: holder.setAttribute("help-type", "section");
1239: } else {
1240: holder.setAttribute("help-type", "page");
1241: }
1242: holder.setAttribute("name", getName());
1243: if (getId().startsWith("V")) {
1244: holder.setAttribute("virtual", "yes");
1245: }
1246: holder.setAttribute("id", getId());
1247: holder.setAttribute("page-id", getPageId());
1248:
1249: PropertyParser pp = new PropertyParser(context, false);
1250: if (url != null) {
1251: holder.setAttribute("href", pp.parse(url));
1252: } else if (menuHelpTopics != null
1253: && CT_URL.equals(menuHelpTopics.getType())) {
1254: holder.setAttribute("href", pp.parse(menuHelpTopics
1255: .getTopicUrl()));
1256: }
1257: }
1258:
1259: public Collection getPath(Context context) {
1260: Collection ret = new ArrayList();
1261: if (parent == null) {
1262: Menu[] menuPath = owner.getPath();
1263: for (int i = 0; i < menuPath.length; ++i) {
1264: ret.add(menuPath[i].getHelp(context));
1265: }
1266: } else {
1267: ret.addAll(parent.getPath(context));
1268: ret.add(this );
1269: }
1270: return ret;
1271: }
1272:
1273: private class SearchEntry implements Comparable, DomSerializable {
1274: private int relevance;
1275: private Context context;
1276:
1277: public int compareTo(Object o) {
1278: if (o == null) {
1279: return -1;
1280: }
1281: if (o == this ) {
1282: return 0;
1283: }
1284: if (o instanceof SearchEntry) {
1285: if (((SearchEntry) o).relevance != relevance) {
1286: return ((SearchEntry) o).relevance - relevance;
1287: }
1288:
1289: return getName().compareTo(((SearchEntry) o).getName());
1290: }
1291:
1292: return hashCode() - o.hashCode();
1293: }
1294:
1295: String getName() {
1296: String ret = Help.this .getName();
1297: return ret == null ? "" : ret;
1298: }
1299:
1300: public void toDom(Element holder) {
1301: toDomShallow(context, holder);
1302: holder.setAttribute("relevance", String.valueOf(relevance));
1303: }
1304:
1305: public SearchEntry(int relevance, Context context) {
1306: super ();
1307: this .relevance = relevance;
1308: this .context = context;
1309: }
1310:
1311: }
1312:
1313: public void search(Collection terms, Context context,
1314: Collection findings) {
1315: synchronized (this ) {
1316: Map wordCounter = (Map) (words == null ? null : words.get());
1317: if (wordCounter == null) {
1318: wordCounter = new HashMap();
1319: words = new SoftReference(wordCounter);
1320:
1321: if (getName() != null) {
1322: StringTokenizer st = new StringTokenizer(getName());
1323: while (st.hasMoreElements()) {
1324: incCount(wordCounter, st.nextToken(), 10);
1325: }
1326: }
1327:
1328: if (getTitle() != null) {
1329: StringTokenizer st = new StringTokenizer(getTitle());
1330: while (st.hasMoreElements()) {
1331: incCount(wordCounter, st.nextToken(), 5);
1332: }
1333: }
1334:
1335: if (menuHelpTopics != null
1336: && CT_CONTENT.equals(menuHelpTopics.getType())
1337: && !isSection()) {
1338: try {
1339: HTMLLexer lexer = new HTMLLexer(
1340: new StringReader(getContent(1, context,
1341: false, true, false)));
1342: StringBuffer wordBuffer = new StringBuffer();
1343: for (Token t = lexer.nextToken(); t != null
1344: && t.getType() != HTMLTokenTypes.CHTML
1345: && t.getType() != HTMLTokenTypes.EOF; t = lexer
1346: .nextToken()) {
1347: if (t.getText() != null
1348: && ((t.getType() == HTMLTokenTypes.UNDEFINED_TOKEN && t
1349: .getText().length() == 1) || t
1350: .getType() == HTMLTokenTypes.PCDATA)) {
1351: if (t.getText().length() == 1
1352: && Character.isWhitespace(t
1353: .getText().charAt(0))) {
1354: String word = wordBuffer.toString()
1355: .toLowerCase().trim();
1356: wordBuffer = new StringBuffer();
1357: incCount(wordCounter, word, 1);
1358: } else {
1359: wordBuffer.append(t.getText());
1360: }
1361: } else {
1362: String word = wordBuffer.toString()
1363: .toLowerCase();
1364: wordBuffer = new StringBuffer();
1365: incCount(wordCounter, word, 1);
1366: }
1367: }
1368: } catch (Exception e) {
1369: logger.warn("Exception during search: " + e, e);
1370: }
1371: }
1372: }
1373:
1374: int relevance = 1;
1375: Iterator it = terms.iterator();
1376: while (it.hasNext()) {
1377: String term = (String) it.next();
1378:
1379: if (term.startsWith("+") && term.length() > 1) {
1380: int[] cnt = (int[]) wordCounter.get(term
1381: .substring(1));
1382: if (cnt == null) {
1383: relevance = 1;
1384: break;
1385: } else {
1386: relevance *= (cnt[0] + 2);
1387: }
1388: } else if (term.startsWith("-") && term.length() > 1) {
1389: int[] cnt = (int[]) wordCounter.get(term
1390: .substring(1));
1391: if (cnt != null) {
1392: relevance = 1;
1393: break;
1394: }
1395: } else {
1396: int[] cnt = (int[]) wordCounter.get(term);
1397: if (cnt != null) {
1398: relevance *= (cnt[0] + 1);
1399: }
1400: }
1401: }
1402: if (relevance > 1) {
1403: findings.add(new SearchEntry(relevance, context));
1404: }
1405: }
1406:
1407: Iterator it = children.values().iterator();
1408: while (it.hasNext()) {
1409: Help child = (Help) it.next();
1410: if (!child.isSection()) {
1411: child.search(terms, context, findings);
1412: }
1413: }
1414:
1415: if (parent == null) {
1416: it = owner.getChildren().iterator();
1417: while (it.hasNext()) {
1418: Menu child = (Menu) it.next();
1419: child.getHelp(context).search(terms, context, findings);
1420: }
1421: }
1422:
1423: }
1424:
1425: private static void incCount(Map wordCounter, String word,
1426: int increment) {
1427: if (word.length() > 0) {
1428: int[] count = (int[]) wordCounter.get(word);
1429: if (count == null) {
1430: count = new int[] { 0 };
1431: wordCounter.put(word, count);
1432: }
1433: count[0] += increment;
1434: }
1435: }
1436:
1437: }
|