[jboss-cvs] jboss-seam/examples/wiki/src/main/org/jboss/seam/wiki/core/engine ...
Christian Bauer
christian at hibernate.org
Thu Mar 22 08:47:50 EDT 2007
User: cbauer
Date: 07/03/22 08:47:50
Added: examples/wiki/src/main/org/jboss/seam/wiki/core/engine
WikiLink.java WikiTextParser.java
DefaultWikiLinkResolver.java WikiLinkResolver.java
WikiTextRenderer.java
Log:
Cleaned up wiki core link engine
Revision Changes Path
1.1 date: 2007/03/22 12:47:50; author: cbauer; state: Exp;jboss-seam/examples/wiki/src/main/org/jboss/seam/wiki/core/engine/WikiLink.java
Index: WikiLink.java
===================================================================
package org.jboss.seam.wiki.core.engine;
import org.jboss.seam.wiki.core.model.Node;
/**
* Simple value holder for link resolution and rendering.
*
* @author Christian Bauer
*/
public class WikiLink {
Node node;
boolean requiresUpdating = false;
String url;
String description;
boolean broken = false;
boolean external = false;
public WikiLink(boolean broken, boolean external) {
this.broken = broken;
this.external = external;
}
public Node getNode() { return node; }
public void setNode(Node node) { this.node = node; }
public String getUrl() { return url; }
public void setUrl(String url) { this.url = url; }
public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }
public boolean isBroken() { return broken; }
public boolean isExternal() { return external; }
public boolean isRequiresUpdating() { return requiresUpdating; }
public void setRequiresUpdating(boolean requiresUpdating) { this.requiresUpdating = requiresUpdating; }
public String toString() {
return "Node:" + node + " Description: " + description + " URL: " + url;
}
}
1.1 date: 2007/03/22 12:47:50; author: cbauer; state: Exp;jboss-seam/examples/wiki/src/main/org/jboss/seam/wiki/core/engine/WikiTextParser.java
Index: WikiTextParser.java
===================================================================
package org.jboss.seam.wiki.core.engine;
import org.jboss.seam.text.SeamTextParser;
import org.jboss.seam.text.SeamTextLexer;
import org.jboss.seam.wiki.core.model.File;
import org.jboss.seam.wiki.core.model.Document;
import org.jboss.seam.wiki.core.model.Directory;
import org.jboss.seam.wiki.util.WikiUtil;
import org.jboss.seam.Component;
import antlr.ANTLRException;
import java.util.Map;
import java.util.ArrayList;
import java.util.List;
import java.util.HashMap;
import java.io.StringReader;
/**
* Parses SeamText markup and also resolves link and macro tags as wiki links and wiki plugins.
* <p>
* Requires and looks up the contextual variables <tt>currentDocument</tt> and <tt>currentDirectory</tt>.
* Picks the <tt>WikiLinkResolver</tt> present in the contextual variable <tt>wikiLinkResolver</tt>. Calls
* out to a <tt>WikiRender</tt> for the actual in-document rendering of wiki links and wiki plugins. Might update
* the <tt>currentDocument</tt>'s content, this change should be flushed to the datastore after calling
* the parser.
* </p><p>
* After parsing, all links to attachments and all external engine are pushed onto the renderer, where they
* can be used to render an attachment list or similar.
*
* @author Christian Bauer
*/
public class WikiTextParser extends SeamTextParser {
private WikiTextRenderer renderer;
private WikiLinkResolver resolver;
private Directory currentDirectory;
private Document currentDocument;
private Map<String, WikiLink> resolvedLinks = new HashMap<String, WikiLink>();
private List<WikiLink> attachments = new ArrayList<WikiLink>();
private List<WikiLink> externalLinks = new ArrayList<WikiLink>();
public WikiTextParser(String wikiText, WikiTextRenderer renderer) {
super(new SeamTextLexer(new StringReader(wikiText)));
this.renderer = renderer;
resolver = (WikiLinkResolver)Component.getInstance("wikiLinkResolver");
currentDocument = (Document)Component.getInstance("currentDocument");
if (currentDocument == null) throw new RuntimeException("WikiTextParser needs the currentDocument context variable");
currentDirectory = (Directory)Component.getInstance("currentDirectory");
if (currentDirectory == null) throw new RuntimeException("WikiTextParser needs the currentDirectory context variable");
}
/**
* Start parsing the wiki text and resolve wiki links and wiki plugins.
* <p>
* If <tt>updateResolvedLinks</tt> is enabled, the <t>currentDocument</tt>'s content will
* be udpated after parsing the wiki text. This only occurs if we hit a link during link
* resolution that needs to be updated. You should flush this modification do the data store.
*
* @param updateResolvedLinks Set updated content on <tt>currentDocument</tt>
*/
public void parse(boolean updateResolvedLinks) {
try {
startRule();
if (updateResolvedLinks) {
for (Map.Entry<String, WikiLink> entry: resolvedLinks.entrySet()) {
if(entry.getValue().isRequiresUpdating()) {
// One of the links we parsed and resolved requires updating of the current document, run
// the protocol converter - which is usally only called when storing a document.
currentDocument.setContent(
resolver.convertToWikiProtocol(currentDirectory.getAreaNumber(), currentDocument.getContent())
);
// Yes, this might happen during rendering, we flush() and UPDATE the document!
break; // One is enough
}
}
}
renderer.setAttachmentLinks(attachments);
renderer.setExternalLinks(externalLinks);
}
catch (ANTLRException re) {
// TODO: Do we ever get this exception?
throw new RuntimeException(re);
}
}
protected String linkTag(String descriptionText, String linkText) {
resolver.resolveLinkText(currentDirectory.getAreaNumber(), resolvedLinks, linkText);
WikiLink link = resolvedLinks.get((linkText));
if (link == null) return "";
// Override the description of the WikiLink with description found in tag
String finalDescriptionText =
(descriptionText!=null && descriptionText.length() > 0 ? descriptionText : link.getDescription());
link.setDescription(finalDescriptionText);
// Link to file (inline or attached)
if (WikiUtil.isFile(link.getNode())) {
File file = (File)link.getNode();
if (file.getImageMetaInfo() == null || 'A' == file.getImageMetaInfo().getThumbnail()) {
// It's an attachment
if (!attachments.contains(link)) attachments.add(link);
return renderer.renderFileAttachmentLink((attachments.indexOf(link)+1), link);
} else {
// It's an embedded thumbnail
return renderer.renderThumbnailImageInlineLink(link);
}
}
// External link
if (link.isExternal()) {
if (!externalLinks.contains(link)) externalLinks.add(link);
return renderer.renderExternalLink(link);
}
// Regular link
return renderer.renderInlineLink(link);
}
protected String macroInclude(String macroName) {
// Filter out any dangerous characters
return renderer.renderMacro(macroName.replaceAll("[^\\p{Alnum}]+", ""));
}
}
1.1 date: 2007/03/22 12:47:50; author: cbauer; state: Exp;jboss-seam/examples/wiki/src/main/org/jboss/seam/wiki/core/engine/DefaultWikiLinkResolver.java
Index: DefaultWikiLinkResolver.java
===================================================================
package org.jboss.seam.wiki.core.engine;
import org.jboss.seam.annotations.Name;
import org.jboss.seam.annotations.In;
import org.jboss.seam.annotations.Transactional;
import org.jboss.seam.annotations.AutoCreate;
import org.jboss.seam.wiki.core.model.Directory;
import org.jboss.seam.wiki.core.dao.NodeDAO;
import org.jboss.seam.wiki.core.model.*;
import org.jboss.seam.wiki.util.WikiUtil;
import java.util.regex.Pattern;
import java.util.regex.Matcher;
import java.util.Map;
/**
* A default implementation of <tt>WikiLinkResolver</tt>.
*
* @author Christian Bauer
*/
@Name("wikiLinkResolver")
@AutoCreate
public class DefaultWikiLinkResolver implements WikiLinkResolver {
// Render these strings whenever [=>wiki://123] needs to be resolved but can't
public static final String BROKENLINK_URL = "PageDoesNotExist";
public static final String BROKENLINK_DESCRIPTION = "?BROKEN LINK?";
@In
private NodeDAO nodeDAO;
public String convertToWikiProtocol(Long currentAreaNumber, String wikiText) {
if (wikiText == null) return null;
StringBuffer replacedWikiText = new StringBuffer(wikiText.length());
Matcher matcher = Pattern.compile(REGEX_WIKILINK_FORWARD).matcher(wikiText);
// Replace with [Link Text=>wiki://<node id>] or leave as is if not found
while (matcher.find()) {
String linkText = matcher.group(2);
Node node = resolveCrossAreaLinkText(currentAreaNumber, linkText);
if (node != null) matcher.appendReplacement(replacedWikiText, "[$1=>wiki://" + node.getId() + "]");
}
matcher.appendTail(replacedWikiText);
return replacedWikiText.toString();
}
public String convertFromWikiProtocol(Long currentAreaNumber, String wikiText) {
if (wikiText == null) return null;
StringBuffer replacedWikiText = new StringBuffer(wikiText.length());
Matcher matcher = Pattern.compile(REGEX_WIKILINK_REVERSE).matcher(wikiText);
// Replace with [Link Text=>Page Name] or replace with BROKENLINK "page name"
while (matcher.find()) {
// Find the node by PK
Node node = nodeDAO.findNode(Long.valueOf(matcher.group(2)));
// Node is in current area, just use its name
if (node != null && node.getAreaNumber().equals(currentAreaNumber)) {
matcher.appendReplacement(replacedWikiText, "[$1=>" + node.getName() + "]");
// Node is in different area, prepend the area name
} else if (node != null && !node.getAreaNumber().equals(currentAreaNumber)) {
Directory area = nodeDAO.findArea(node.getAreaNumber());
matcher.appendReplacement(replacedWikiText, "[$1=>" + area.getName() + "|" + node.getName() + "]");
// Couldn't find it anymore, its a broken link
} else {
matcher.appendReplacement(replacedWikiText, "[$1=>" + BROKENLINK_DESCRIPTION + "]");
}
}
matcher.appendTail(replacedWikiText);
return replacedWikiText.toString();
}
@Transactional
public void resolveLinkText(Long currentAreaNumber, Map<String, WikiLink> links, String linkText) {
// Don't resolve twice
if (links.containsKey(linkText)) return;
Matcher wikiProtocolMatcher = Pattern.compile(REGEX_WIKI_PROTOCOL).matcher(linkText);
Matcher knownProtocolMatcher = Pattern.compile(REGEX_KNOWN_PROTOCOL).matcher(linkText);
WikiLink wikiLink;
// Check if its a common protocol
if (knownProtocolMatcher.find()) {
wikiLink = new WikiLink(false, true);
wikiLink.setUrl(linkText);
wikiLink.setDescription(linkText);
// Check if it is a wiki protocol
} else if (wikiProtocolMatcher.find()) {
// Find the node by PK
Node node = nodeDAO.findNode(Long.valueOf(wikiProtocolMatcher.group(1)));
if (node != null) {
wikiLink = new WikiLink(false, false);
wikiLink.setNode(node);
wikiLink.setDescription(node.getName());
} else {
// Can't do anything, [=>wiki://123] no longer exists
wikiLink = new WikiLink(true, false);
wikiLink.setUrl(BROKENLINK_URL);
wikiLink.setDescription(BROKENLINK_DESCRIPTION);
}
// It must be a stored clear text link, such as [=>Target Name] or [=>Area Name|Target Name]
// (This can happen if the string [foo=>bar] or [foo=>bar|baz] was stored in the database because the
// targets didn't exist at the time of saving)
} else {
// Try a WikiWord search in the current or named area
Node node = resolveCrossAreaLinkText(currentAreaNumber, linkText);
if (node!=null) {
wikiLink = new WikiLink(false, false);
wikiLink.setNode(node);
wikiLink.setDescription(node.getName());
// Indicate that caller should update the wiki text that contains this link
wikiLink.setRequiresUpdating(true);
} else {
/* TODO: Not sure we should actually implement this..., one of these things that the wiki "designers" got wrong
// OK, so it's not any recognized URL and we can't find a node with that wikiname
// Let's assume its a page name and render /Area/WikiLink (but encoded, so it gets transported fully)
// into the edit page when the user clicks on the link to create the document
try {
String encodedPagename = currentDirectory.getWikiname() + "/" + URLEncoder.encode(linkText, "UTF-8");
wikiLink = new WikiLink(null, true, encodedPagename, linkText);
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e); // Java is so great...
}
*/
wikiLink = new WikiLink(true, false);
wikiLink.setUrl(BROKENLINK_URL);
wikiLink.setDescription(BROKENLINK_DESCRIPTION);
}
}
links.put(linkText, wikiLink);
}
private Node resolveCrossAreaLinkText(Long currentAreaNumber, String linkText) {
Matcher crossLinkMatcher = Pattern.compile(REGEX_WIKILINK_CROSSAREA).matcher(linkText);
if (crossLinkMatcher.find()) {
// Try to find the node in the referenced area
String areaName = crossLinkMatcher.group(1);
String nodeName = crossLinkMatcher.group(2);
Node crossLinkArea = nodeDAO.findArea(WikiUtil.convertToWikiName(areaName));
if ( crossLinkArea != null && (nodeName == null || nodeName.length() == 0) )
return crossLinkArea; // Support [=>This is an Area Link|] syntax
else if (crossLinkArea != null)
return nodeDAO.findNodeInArea(crossLinkArea.getAreaNumber(), WikiUtil.convertToWikiName(nodeName));
} else {
// Try the current area
return nodeDAO.findNodeInArea(currentAreaNumber, WikiUtil.convertToWikiName(linkText));
}
return null;
}
}
1.1 date: 2007/03/22 12:47:50; author: cbauer; state: Exp;jboss-seam/examples/wiki/src/main/org/jboss/seam/wiki/core/engine/WikiLinkResolver.java
Index: WikiLinkResolver.java
===================================================================
package org.jboss.seam.wiki.core.engine;
import java.util.Map;
import java.util.regex.Pattern;
/**
* The heart of the wiki, converts and resolves human-readable link tags from and to permanent
* links that can be stored (or read) persistently. Also resolves link texts (from stored
* link tags) to <tt>WikiLink</tt> objects, for rendering.
* <p>
* Use the supplied regular expressions to implement the methods, or parse the wiki text
* completely by hand and convert/resolve links.
*
* @author Christian Bauer
*/
public interface WikiLinkResolver {
/**
* Matches known protocols, e.g. [=>http://foo.bar], which can be ignored and "resolved" as-is
*/
public static final String REGEX_KNOWN_PROTOCOL = "(http://)|(https://)|(ftp://)|(mailto:)";
/**
* Prepended to primary identifiers when links are stored, e.g. [This is a stored link=>wiki://5]
*/
public static final String REGEX_WIKI_PROTOCOL = "wiki://([0-9]+)";
/**
* Match [GROUP1=>GROUP1], used to replace links from user input with wiki:// URLs - used
* in <tt>convertToWikiProtocol()</tt>.
*/
public static final String REGEX_WIKILINK_FORWARD =
Pattern.quote("[") + "([^" + Pattern.quote("]") + "|" + Pattern.quote("[") + "]*)" +
"=>([^(?://)@" + Pattern.quote("]") + Pattern.quote("[") + "]+)" + Pattern.quote("]");
/**
* Match [GROUP1=>wiki://GROUP2], used to replace wiki:// URLs with page names
*/
public static final String REGEX_WIKILINK_REVERSE =
Pattern.quote("[") + "([^" + Pattern.quote("]") + "|" + Pattern.quote("[") + "]*)" +
"=>" + REGEX_WIKI_PROTOCOL + Pattern.quote("]");
/**
* Match "Foo Bar|Baz Brrr" as two groups
*/
public static final String REGEX_WIKILINK_CROSSAREA = "^(.+)" + Pattern.quote("|") + "(.*)$";
/**
* Replaces clear text links such as <tt>[=>Target Name]</tt> in <tt>wikiText</tt> with
* <tt>[=>wiki://id]</tt> strings, usually resolves the target name as a unique wiki name in some data store.
* The <tt>currentAreaNumber</tt> of the current document is supplied and can be used as the namespace for scoped resolving.
* <p>
* This method should be called whenever a wiki document is stored, we want to store the permanent
* identifiers of a target node. That way, the target node can be renamed and the document that links
* to that target node still contains the valid link.
* </p><p>
* Either parse the <tt>wikiText</tt> by hand to find and replace links, or use the
* <tt>REGEX_WIKILINK_FORWARD</tt> pattern which matches <tt>[GROUP1=>GROUP2]</tt>.
* Convert the target name (<tt>GROUP2</tt>) to a unique wiki name, and then to some primary
* identifier which you can lookup again in the future in a reliable fashion. <tt>GROUP1</tt> is
* the optional link description entered by the user, you need to keep this string and only replace
* <tt>GROUP2</tt> with a permanent identifier (prefixed with the <tt>wiki://</tt> protocol).
* </p><p>
* Note that cross-namespace linking should be supported, so in addition to <tt>[=>Target Name]</tt>,
* links can be entered by the user as <tt>[=>Target Area|Target Name]</tt>. To resolve these link
* texts, use <tt>REGEX_WIKILINK_CROSSAREA</tt> on the original <tt>GROUP1</tt>, which produces
* two groups. Ignore the given <tt>currentAreaNumber</tt> parameter and resolve in the target namespace entered by
* the user on the link tag.
* </p><p>
* Example pseudo code:
* </p><p>
* <pre>
* if (targetName = wikiText.match(REGEX_WIKI_LINK_FORWARD)) {
*
* if (targetNamespace, newTargetName = targetName.match(REGEX_WIKILINK_CROSSAREA) {
*
* wikiText.replace( resolveNodeId(targetNamespace, newTargetName) );
*
* } else {
* wikiText.replace( resolveNodeId(givenNamespace, targetName) );
* }
* }
* </pre>
*
* @param currentAreaNumber The currennt area useable as the namespace for scoped resolving
* @param wikiText Text with wiki markup containing [=>Target Name] links
* @return The <tt>wikiText</tt> with all <tt>[=>Target Name]<tt> links replaced with <tt>[=>wiki://id]</tt>
*/
public String convertToWikiProtocol(Long currentAreaNumber, String wikiText);
/**
* Replace stored text links such as <tt>[Link description=>wiki://id]</tt> with clear text target names, so
* users can edit the link again in clear text.
* </p><p>
* Either parse by hand or use the <tt>REGEX_WIKILINK_REVERSE</tt> pattern, which matches
* <tt>[GROUP1=>wiki://GROUP2]. Replace with <tt>[GROUP1=>Target Name]</tt> or, if the target is not in
* the same namespace as the given <tt>area</tt> parameter, append the area:
* <tt>[GROUP1=>Target Area|Target Name]</tt>.
*
* @param currentAreaNumber The current area useable as the namespace for scoped resolving
* @param wikiText Text with wiki markup containing [=>wiki://id] links
* @return The <tt>wikiText</tt> with all <tt>[=>wiki://id]<tt> links replaced with <tt>[=>Target Name]</tt>
*/
public String convertFromWikiProtocol(Long currentAreaNumber, String wikiText);
/**
* Resolve the given <tt>linkText</tt> to an instance of <tt>WikiLink</tt> and put it in the <tt>link</tt> map.
* <p>
* The <tt>WikiLink</tt> objects are used during rendering, the rules are as follows:
* <ul>
* <li>If the <tt>linkText</tt> matches <tt>REGEX_KNOWN_PROTOCOL</tt>, don't resolve but create
* a <tt>WikiLink</tt> instance that contains <tt>url</tt>, <tt>description</tt> (same as <tt>url</tt>),
* <tt>broken=false</tt>, <tt>external=true</tt>. The <tt>url</tt> is the actual <tt>linkText</tt>, as-is.
* </li>
* <li>If the <tt>linkText</tt> matches <tt>REGEX_WIKI_PROTOCOL</tt>, resolve it and create
* a <tt>WikiLink</tt> instance that contains the resolved <tt>Node</tt> instance, the node name
* as <tt>description</tt>, no <tt>url</tt>, and <tt>external=false</tt>. If the <tt>linkText</tt>
* can't be resolved to a <tt>Node</tt>, set <tt>broken=true</tt>, a null <tt>node</tt>, and whatever
* <tt>url</tt> and <tt>description</tt> you want to render for a broken link.
* </li>
* <li>Otherwise, the <tt>linkText</tt> represents a clear text link such as <tt>Target Name</tt> or
* <tt>Target Area|TargetName</tt>, which you can resolve if you want and return a
* <tt>WikiLink</tt> instance as in the previous rule. If it can't be resolved, return a broken link
* indicator as described in the previous rule. If it has been resolved, you may indicate that the
* original document that contains this <tt>linkText</tt> should be updated in the datastore (usually
* by passing its wiki text content through <tt>convertToWikiProtocol</tt>) - set <tt>requiresUpdating=true</tt>
* on the <tt>WikiLink</tt> instance. It's the job of the client of this resolver to handle this flag
* (or ignore it).
*</li>
* </ul>
*
* @param currentAreaNumber The current area useable as the namespace for scoped resolving
* @param links A map of all resolved <tt>WikiLink</tt> objects, keyed by <tt>linkText</tt>
* @param linkText A stored link text, such as "wiki://123" or "http://foo.bar" or "Target Area|Target Name]"
*/
public void resolveLinkText(Long currentAreaNumber, Map<String, WikiLink> links, String linkText);
}
1.1 date: 2007/03/22 12:47:50; author: cbauer; state: Exp;jboss-seam/examples/wiki/src/main/org/jboss/seam/wiki/core/engine/WikiTextRenderer.java
Index: WikiTextRenderer.java
===================================================================
package org.jboss.seam.wiki.core.engine;
import java.util.List;
/**
* Called by the WikiTextParser to render [A Link=>Target] and [<=MacroName].
*
* @author Christian Bauer
*/
public interface WikiTextRenderer {
public String renderInlineLink(WikiLink inlineLink);
public String renderExternalLink(WikiLink externalLink);
public String renderThumbnailImageInlineLink(WikiLink inlineLink);
public String renderFileAttachmentLink(int attachmentNumber, WikiLink attachmentLink);
public void setAttachmentLinks(List<WikiLink> attachmentLinks);
public void setExternalLinks(List<WikiLink> externalLinks);
public String renderMacro(String macroName);
}
More information about the jboss-cvs-commits
mailing list