[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