jspwiki-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From ajaqu...@apache.org
Subject svn commit: r771072 [1/2] - in /incubator/jspwiki/trunk: ChangeLog src/java/org/apache/wiki/ReferenceManager.java src/java/org/apache/wiki/Release.java tests/java/org/apache/wiki/ReferenceManagerTest.java
Date Sun, 03 May 2009 16:04:00 GMT
Author: ajaquith
Date: Sun May  3 16:04:00 2009
New Revision: 771072

URL: http://svn.apache.org/viewvc?rev=771072&view=rev
Log:
Second commit of ReferenceManager. All unit tests now pass, although certain behaviors are slightly different than in the past. In particular, the WikiPaths passed to getReferredBy() and getRefersTo() are NOT resolved; they are assumed to be correct. This was an unnecessary optimization that, in practice, was not needed. Overall pass rate over 93%.

Modified:
    incubator/jspwiki/trunk/ChangeLog
    incubator/jspwiki/trunk/src/java/org/apache/wiki/ReferenceManager.java
    incubator/jspwiki/trunk/src/java/org/apache/wiki/Release.java
    incubator/jspwiki/trunk/tests/java/org/apache/wiki/ReferenceManagerTest.java

Modified: incubator/jspwiki/trunk/ChangeLog
URL: http://svn.apache.org/viewvc/incubator/jspwiki/trunk/ChangeLog?rev=771072&r1=771071&r2=771072&view=diff
==============================================================================
--- incubator/jspwiki/trunk/ChangeLog (original)
+++ incubator/jspwiki/trunk/ChangeLog Sun May  3 16:04:00 2009
@@ -1,6 +1,6 @@
 2009-05-03  Andrew Jaquith <ajaquith AT apache DOT org>
 
-        * 3.0.0-svn-108
+        * 3.0.0-svn-109
 
         * Second commit of ReferenceManager. All unit tests now pass, although certain
         behaviors are slightly different than in the past. In particular, the WikiPaths

Modified: incubator/jspwiki/trunk/src/java/org/apache/wiki/ReferenceManager.java
URL: http://svn.apache.org/viewvc/incubator/jspwiki/trunk/src/java/org/apache/wiki/ReferenceManager.java?rev=771072&r1=771071&r2=771072&view=diff
==============================================================================
--- incubator/jspwiki/trunk/src/java/org/apache/wiki/ReferenceManager.java (original)
+++ incubator/jspwiki/trunk/src/java/org/apache/wiki/ReferenceManager.java Sun May  3 16:04:00 2009
@@ -51,12 +51,13 @@
  * </p>
  * <p>
  * ReferenceManager stores outbound links ("refers to") with the WikiPage that
- * originates the link, as the multi-valued property named {@value #REFERS_TO}}.
- * Inbound links ("referred by") are stored in a separate part of the content
- * repository, in the special path {@value #REFERENCES_ROOT}, where each node
- * represents the page that is being linked to (<em>i.e,</em> a page link
- * named in wiki markup). The multi-valued property named {@value #REFERRED_BY}
- * stores the source of the link.
+ * originates the link, as the multi-valued property named
+ * {@value #PROPERTY_REFERS_TO}}. Inbound links ("referred by") are stored in a
+ * separate part of the content repository, in the special path
+ * {@value #REFERENCES_ROOT}, where each node represents the page that is being
+ * linked to (<em>i.e,</em> a page link named in wiki markup). The
+ * multi-valued property named {@value #PROPERTY_REFERRED_BY} stores the source
+ * of the link.
  * </p>
  * <p>
  * To ensure that ReferenceManager operates as efficiently as possible, page
@@ -76,6 +77,8 @@
  * WikiPage but haven't been created yet (uncreated); and the names of pages
  * that have been created but not linked to by any other others (unreferenced).
  * These lists are updated whenever wiki pages are saved, renamed or deleted.
+ * The JCR paths for uncreated and unreferenced pages are in
+ * {@link #NOT_CREATED} and {@link #NOT_REFERENCED}, respectively.
  * </p>
  * <p>
  * It is always possible that, despite JSPWiki's best efforts, that the link
@@ -86,540 +89,385 @@
 public class ReferenceManager implements InternalModule, WikiEventListener
 {
 
-    /** The WikiEngine that owns this object. */
-    private WikiEngine m_engine;
-
-    private ContentManager m_cm;
-
-    private boolean m_camelCase = false;
-
-    private boolean m_matchEnglishPlurals = false;
-
-    public static final String REFERS_TO = "wiki:refersTo";
-
     /** We use this also a generic serialization id */
     private static final long serialVersionUID = 4L;
 
-    protected static final String REFERENCES_ROOT = "/wiki:references";
-
-    protected static final String NOT_REFERENCED = "notReferenced";
+    private static final String PROPERTY_NOT_CREATED = "notCreated";
 
-    protected static final String NOT_CREATED = "notCreated";
-
-    private static final List<WikiPath> NO_LINKS = Collections.emptyList();
+    private static final String PROPERTY_NOT_REFERENCED = "notReferenced";
 
     private static final String[] NO_VALUES = new String[0];
-    
 
     private static final Pattern LINK_PATTERN = Pattern
         .compile( "([\\[\\~]?)\\[([^\\|\\]]*)(\\|)?([^\\|\\]]*)(\\|)?([^\\|\\]]*)\\]" );
 
-    /**
-     * JCR path path prefix for inbound "referredby" links, used by
-     * {@link #addReferredBy(WikiPath, List)}.
-     */
-    private static final String REFERRED_BY = "wiki:referredBy";
+    protected static final String REFERENCES_ROOT = "/wiki:references";
 
-    /**
-     * Default constructor that creates a new ReferenceManager. Callers must
-     * should call {@link #initialize(WikiEngine, Properties)} to activate the
-     * ReferenceManager.
-     */
-    public ReferenceManager()
-    {
-        // Do nothing, really.
-        super();
-    }
+    protected static final String PROPERTY_REFERRED_BY = "wiki:referredBy";
+
+    protected static final String PROPERTY_REFERS_TO = "wiki:refersTo";
 
     /**
-     * Rebuilds the internal references database by parsing every wiki page.
-     * 
-     * @throws RepositoryException
-     * @throws LoginException
+     * JCR path path prefix for inbound "referredby" links, used by
+     * {@link #addReferredBy(WikiPath, WikiPath)}. Absolute path whose prefix is
+     * {@link #REFERENCES_ROOT}.
      */
-    public void rebuild() throws RepositoryException
-    {
-        // Remove all of the 'referencedBy' inbound links
-        ContentManager cm = m_engine.getContentManager();
-        Session s = cm.getCurrentSession();
+    protected static final String REFERRED_BY = REFERENCES_ROOT + "/wiki:referrers";
 
-        if( s.getRootNode().hasNode( REFERENCES_ROOT ) )
-        {
-            Node nd = s.getRootNode().getNode( REFERENCES_ROOT );
-            nd.remove();
-        }
-        s.save();
+    protected static final String NOT_REFERENCED = REFERENCES_ROOT + "/wiki:notReferenced";
 
-        initReferredByNodes();
-        s.save();
-        cm.release();
+    protected static final String NOT_CREATED = REFERENCES_ROOT + "/wiki:notCreated";
 
-        // TODO: we should actually parse the pages
-    }
+    protected static final String[] REFERENCES_METADATA = { REFERRED_BY, NOT_REFERENCED, NOT_CREATED };
 
     /**
-     * Verifies that the JCR nodes for storing references exist, and creates
-     * then if they do not. The nodes are NOT saved; that is the responsibility
-     * of callers.
+     * Replaces camelcase links.
      */
-    private void initReferredByNodes() throws RepositoryException
+    private static String renameCamelCaseLinks( String sourceText, String from, String to )
     {
-        ContentManager cm = m_engine.getContentManager();
-        Session s = cm.getCurrentSession();
-        if( !s.getRootNode().hasNode( REFERENCES_ROOT ) )
-        {
-            s.getRootNode().addNode( REFERENCES_ROOT );
-        }
-    }
+        StringBuilder sb = new StringBuilder( sourceText.length() + 32 );
 
-    /**
-     * Initializes the reference manager. Scans all existing WikiPages for
-     * internal links and adds them to the ReferenceManager object.
-     * 
-     * @param engine The WikiEngine to which this is managing references to.
-     * @param props the properties for initializing the WikiEngine
-     * @throws WikiException If the reference manager initialization fails.
-     */
-    public void initialize( WikiEngine engine, Properties props ) throws WikiException
-    {
-        m_engine = engine;
-        m_cm = engine.getContentManager();
+        Pattern linkPattern = Pattern.compile( "\\p{Lu}+\\p{Ll}+\\p{Lu}+[\\p{L}\\p{Digit}]*" );
 
-        m_matchEnglishPlurals = TextUtil.getBooleanProperty( engine.getWikiProperties(), WikiEngine.PROP_MATCHPLURALS,
-                                                             m_matchEnglishPlurals );
+        Matcher matcher = linkPattern.matcher( sourceText );
 
-        m_camelCase = TextUtil.getBooleanProperty( m_engine.getWikiProperties(), JSPWikiMarkupParser.PROP_CAMELCASELINKS, false );
-        try
+        int start = 0;
+
+        while ( matcher.find( start ) )
         {
-            Session session = m_engine.getContentManager().getCurrentSession();
+            String match = matcher.group();
 
-            if( !session.getRootNode().hasNode( REFERENCES_ROOT ) )
-            {
-                session.getRootNode().addNode( REFERENCES_ROOT );
+            sb.append( sourceText.substring( start, matcher.start() ) );
 
-                session.save();
+            int lastOpenBrace = sourceText.lastIndexOf( '[', matcher.start() );
+            int lastCloseBrace = sourceText.lastIndexOf( ']', matcher.start() );
+
+            if( match.equals( from ) && lastCloseBrace >= lastOpenBrace )
+            {
+                sb.append( to );
             }
-        }
-        catch( RepositoryException e )
-        {
-            throw new ProviderException( "Failed to initialize repository contents", e );
+            else
+            {
+                sb.append( match );
+            }
+
+            start = matcher.end();
         }
 
-        // Make sure we catch any page add/save/rename events
-        WikiEventManager.addWikiEventListener( engine.getContentManager(), this );
+        sb.append( sourceText.substring( start ) );
+
+        return sb.toString();
     }
 
     /**
-     * <p>
-     * Removes all links between a source page and one or more destination
-     * pages, and vice-versa. The source page must exist, although the
-     * destinations may not. The modified nodes are saved.
-     * <p>
-     * Within the m_refersTo map the pagename is a key. The whole key-value-set
-     * has to be removed to keep the map clean. Within the m_referredBy map the
-     * name is stored as a value. Since a key can have more than one value we
-     * have to delete just the key-value-pair referring page:deleted page.
-     * </p>
-     * 
-     * @param page Name of the page to remove from the maps.
-     * @throws PageNotFoundException if the source page does not exist
-     * @throws ProviderException
-     * @throws RepositoryException if the links cannot be reset
+     * This method does a correct replacement of a single link, taking into
+     * account anchors and attachments.
      */
-    protected synchronized void removeLinks( WikiPath page ) throws ProviderException, RepositoryException
+    private static String renameLink( String original, String from, String newlink )
     {
-        Session s = m_cm.getCurrentSession();
+        int hash = original.indexOf( '#' );
+        int slash = original.indexOf( '/' );
+        String reallink = original;
+        String oldStyleRealLink;
 
-        // Remove all inbound links TO the page
-        // Let's pretend B and C ---> A
+        if( hash != -1 )
+            reallink = original.substring( 0, hash );
+        if( slash != -1 )
+            reallink = original.substring( 0, slash );
 
-        // First, remove all inbound links from B & C to A
-        String jcrPath = getReferencedByJCRNode( page );
-        List<WikiPath> inboundLinks = getReferredBy( page );
-        for( WikiPath source : inboundLinks )
-        {
-            removeAllFromValues( jcrPath, REFERRED_BY, source.toString() );
-            s.save();
-        }
+        reallink = MarkupParser.cleanLink( reallink );
+        oldStyleRealLink = MarkupParser.wikifyLink( reallink );
 
-        // Remove all outbound links FROM the page
-        // Let's pretend A ---> B and C
+        // WikiPage realPage = context.getEngine().getPage( reallink );
+        // WikiPage p2 = context.getEngine().getPage( from );
 
-        // Remove all inbound links from B &C to A
-        List<WikiPath> outboundLinks = getRefersTo( page );
-        for( WikiPath destination : outboundLinks )
+        // System.out.println(" "+reallink+" :: "+ from);
+        // System.out.println(" "+p+" :: "+p2);
+
+        //
+        // Yes, these point to the same page.
+        //
+        if( reallink.equals( from ) || original.equals( from ) || oldStyleRealLink.equals( from ) )
         {
-            jcrPath = ContentManager.getJCRPath( page );
-            removeAllFromValues( jcrPath, REFERS_TO, destination.toString() );
-            s.save();
+            //
+            // if the original contains blanks, then we should introduce a link,
+            // for example: [My Page] => [My Page|My Renamed Page]
+            int blank = reallink.indexOf( " " );
 
-            jcrPath = ContentManager.getJCRPath( destination );
-            removeAllFromValues( jcrPath, REFERS_TO, page.toString() );
-            s.save();
+            if( blank != -1 )
+            {
+                return original + "|" + newlink;
+            }
 
-            jcrPath = getReferencedByJCRNode( destination );
-            removeAllFromValues( jcrPath, REFERRED_BY, page.toString() );
-            s.save();
+            return newlink + ((hash > 0) ? original.substring( hash ) : "") + ((slash > 0) ? original.substring( slash ) : "");
         }
 
+        return original;
     }
 
     /**
-     * Build the path which is used to store the ReferredBy data
-     */
-    private String getReferencedByJCRNode( WikiPath name )
-    {
-        return "/wiki:references/" + name.getSpace() + "/" + name.getPath();
-    }
-
-    /**
-     * <p>
-     * Returns all pages that refers to a destination page. You can use this as
-     * a quick way of getting the inbound links to a page from other pages. The
-     * page being looked up need not exist. The requested page is not resolved
-     * in any way, so if the page is not found as specified exactly by the path,
-     * a zero-length list will be returned.
-     * </p>
+     * Renames a link in a given source text into a new name, and returns the
+     * transformed text.
      * 
-     * @param destination the page to look up
-     * @return the list of pages that link to this page
-     * @throws ProviderException If something goes wrong
-     * @throws RepositoryException If the referredBy root cannot be checked (or
-     *             created)
-     * @since 3.0
+     * @param sourceText the source text
+     * @param from the link to change, for example, "Main"
+     * @param to the name to change the link to, for example "RenamedMain"
+     * @return the transformed text
      */
-    public List<WikiPath> getReferredBy( WikiPath destination ) throws ProviderException
+    protected static String renameLinks( String sourceText, String from, String to )
     {
-        String jcrPath = getReferencedByJCRNode( destination );
+        StringBuilder sb = new StringBuilder( sourceText.length() + 32 );
 
-        try
+        //
+        // This monstrosity just looks for a JSPWiki link pattern. But it is
+        // pretty
+        // cool for a regexp, isn't it? If you can understand this in a single
+        // reading,
+        // you have way too much time in your hands.
+        //
+        Matcher matcher = LINK_PATTERN.matcher( sourceText );
+
+        int start = 0;
+
+        // System.out.println("====");
+        // System.out.println("SRC="+sourceText.trim());
+        while ( matcher.find( start ) )
         {
-            jcrPath += "/" + REFERRED_BY;
+            char charBefore = (char) -1;
+
+            if( matcher.start() > 0 )
+                charBefore = sourceText.charAt( matcher.start() - 1 );
 
-            Property p = (Property) m_engine.getContentManager().getCurrentSession().getItem( jcrPath );
+            if( matcher.group( 1 ).length() > 0 || charBefore == '~' || charBefore == '[' )
+            {
+                //
+                // Found an escape character, so I am escaping.
+                //
+                sb.append( sourceText.substring( start, matcher.end() ) );
+                start = matcher.end();
+                continue;
+            }
 
-            ArrayList<WikiPath> result = new ArrayList<WikiPath>();
+            String text = matcher.group( 2 );
+            String link = matcher.group( 4 );
+            String attr = matcher.group( 6 );
 
-            for( Value v : p.getValues() )
+            /*
+             * System.out.println("MATCH="+matcher.group(0));
+             * System.out.println(" text="+text); System.out.println("
+             * link="+link); System.out.println(" attr="+attr);
+             */
+            if( link.length() == 0 )
+            {
+                text = renameLink( text, from, to );
+            }
+            else
             {
-                result.add( WikiPath.valueOf( v.getString() ) );
+                link = renameLink( link, from, to );
+
+                //
+                // A very simple substitution, but should work for quite a few
+                // cases.
+                //
+                text = TextUtil.replaceString( text, from, to );
             }
 
-            return result;
-        }
-        catch( PathNotFoundException e )
-        {
-            // Fine, we can return an empty set
-            return Collections.emptyList();
-        }
-        catch( RepositoryException e )
-        {
-            throw new ProviderException( "Unable to get the referred-by list", e );
+            //
+            // Construct the new string
+            //
+            sb.append( sourceText.substring( start, matcher.start() ) );
+            sb.append( "[" + text );
+            if( link.length() > 0 )
+                sb.append( "|" + link );
+            if( attr.length() > 0 )
+                sb.append( "|" + attr );
+            sb.append( "]" );
+
+            start = matcher.end();
         }
+
+        sb.append( sourceText.substring( start ) );
+
+        return sb.toString();
     }
 
-    /**
-     * Adds a "referredBy" inbound link to a page from a source page that links
-     * to it. That is, for the destination page, a "referredBy" entry is made
-     * that contains the name of the source page. Neither the source or
-     * destination pages need exist. This method saves the underlying Node after
-     * processing is complete.
-     * 
-     * @param page the page that is the destination for the link
-     * @param from the page that originates the link
-     * @throws RepositoryException if the underlying JCR node cannot be
-     *             retrieved
-     */
-    protected void addReferredBy( WikiPath page, WikiPath from ) throws RepositoryException
-    {
-        ContentManager cm = m_engine.getContentManager();
-        Session s = cm.getCurrentSession();
+    /** The WikiEngine that owns this object. */
+    private WikiEngine m_engine;
 
-        // Make sure the 'referredBy' root exists
-        initReferredByNodes();
+    private ContentManager m_cm;
 
-        // Set the inverse 'referredBy' link for the destination (referred by
-        // the source)
-        String jcrPath = getReferencedByJCRNode( page );
-        addToValues( jcrPath, REFERRED_BY, from.toString() );
+    private boolean m_camelCase = false;
 
-        // Save the node
-        s.save();
-    }
+    private boolean m_matchEnglishPlurals = false;
 
     /**
-     * Retrieves an array of Strings stored at a given JCR node and
-     * {@link javax.jcr.Property}. The property is assumed to return an array
-     * of {@link javax.jcr.Value} objects. If the node does not exist, a zero-length
-     * array is returned.
-     * 
-     * @param jcrNode the JCR path to the node
-     * @param property the property to read
-     * @throws RepositoryException
+     * Default constructor that creates a new ReferenceManager. Callers must
+     * should call {@link #initialize(WikiEngine, Properties)} to activate the
+     * ReferenceManager.
      */
-    protected String[] getValues( String jcrNode, String property ) throws RepositoryException
+    public ReferenceManager()
     {
-        // Retrieve the destination node for the page
-        ContentManager cm = m_engine.getContentManager();
-        Node node = null;
-        try
-        {
-            node = (Node) cm.getCurrentSession().getItem( jcrNode );
-        }
-        catch( PathNotFoundException e )
-        {
-            return NO_VALUES;
-        }
-
-        // Retrieve the property; re-pack value array into String array
-        String[] stringValues = NO_VALUES;
-        try
-        {
-            Property p = (Property) node.getProperty( property );
-            Value[] values = p.getValues();
-            stringValues = new String[values.length];
-            for( int i = 0; i < values.length; i++ )
-            {
-                stringValues[i] = values[i].getString();
-            }
-        }
-        catch( PathNotFoundException e )
-        {
-            return NO_VALUES;
-        }
-        
-        return stringValues;
+        // Do nothing, really.
+        super();
     }
-    
+
     /**
-     * Adds a single String value to a given JCR node and
-     * {@link javax.jcr.Property}. The property is assumed to return an array
-     * of {@link javax.jcr.Value} objects. The node is created if it does not
-     * exist. Modifications to the node are not saved.
-     * 
-     * @param jcrNode the JCR path to the node
-     * @param property the property to add to
-     * @param value the value to add
-     * @param addAgain whether the value should be added again if it already exists in the list
+     * {@inheritDoc} After the page has been saved, updates the reference lists.
+     * Modifications to the underlying JCR nodes are saved by the current JCR
+     * {@link Session}.
      */
-    protected void addToValues( String jcrNode, String property, String newValue, boolean addAgain ) throws RepositoryException
+    public void actionPerformed( WikiEvent event )
     {
-        // Retrieve (or create) the destination node for the page
-        ContentManager cm = m_engine.getContentManager();
-        Session s = cm.getCurrentSession();
-        Node node = null;
-        try
+        if( !(event instanceof WikiPageEvent) )
         {
-            node = (Node) cm.getCurrentSession().getItem( jcrNode );
+            return;
         }
-        catch( PathNotFoundException e )
+
+        String pageName = ((WikiPageEvent) event).getPageName();
+        if( pageName == null )
         {
-            if( !s.itemExists( jcrNode ) )
-            {
-                node = cm.createJCRNode( jcrNode );
-            }
+            return;
         }
 
-        // Retrieve the property; add value to the end
-        List<String>newValues = new ArrayList<String>();
         try
         {
-            boolean notFound = true;
-            Property p = (Property) node.getProperty( property );
-            Value[] values = p.getValues();
-            for( int i = 0; i < values.length; i++ )
+            switch( event.getType() )
             {
-                newValues.add( values[i].getString() );
-                if ( values[i].equals( newValue ) )
-                {
-                    notFound = false;
+                // ========= page saved ==============================
+
+                // If page was saved, update all references
+                case (ContentEvent.NODE_SAVED ): {
+                    WikiPath path = resolvePage( WikiPath.valueOf( pageName ) );
+
+                    // Get new linked pages, and set refersTo/referencedBy links
+                    List<WikiPath> referenced = extractLinks( path );
+                    setLinks( path, referenced );
+
+                    m_cm.getCurrentSession().save();
+                    break;
                 }
-            }
-            if ( notFound || addAgain )
-            {
-                newValues.add( newValue );
-            }
-            node.setProperty( property, newValues.toArray(new String[newValues.size()]) );
-        }
-        catch( PathNotFoundException e )
-        {
-            node.setProperty( property, new String[]{ newValue } );
-        }
-    }
-    
-    /**
-     * Adds a single String value to a given JCR node and
-     * {@link javax.jcr.Property}. The property is assumed to return an array
-     * of {@link javax.jcr.Value} objects. The node is created if it does not
-     * exist. Modifications to the node are not saved.
-     * 
-     * @param jcrNode the JCR path to the node
-     * @param property the property to add to
-     * @param value the value to add. It it already exists in the list, it will
-     *            be added again.
-     */
-    protected void addToValues( String jcrNode, String property, String newValue ) throws RepositoryException
-    {
-        addToValues( jcrNode, property, newValue, true );
-    }
 
-    /**
-     * Removes a String value from a given JCR node and
-     * {@link javax.jcr.Property}. The property is assumed to return an array
-     * of {@link javax.jcr.Value} objects. The node is created if it does not
-     * exist. Modifications to the node are not saved.
-     * 
-     * @param jcrNode the JCR path to the node
-     * @param property the property to add to
-     * @param value the value to remove. All occurrences of the matching value
-     *            will be removed.
-     */
-    protected void removeAllFromValues( String jcrNode, String property, String value ) throws RepositoryException
-    {
-        // Retrieve (or create) the destination node for the page
-        ContentManager cm = m_engine.getContentManager();
-        Session s = cm.getCurrentSession();
-        Node node = null;
-        try
-        {
-            node = (Node) cm.getCurrentSession().getItem( jcrNode );
-        }
-        catch( PathNotFoundException e )
-        {
-            if( !s.itemExists( jcrNode ) )
-            {
-                node = cm.createJCRNode( jcrNode );
-            }
-        }
+                    // ========= page deleted ==============================
 
-        // Retrieve the property; remove all instances of value
-        List<String> newValues = new ArrayList<String>();
-        try
-        {
-            Property p = (Property) node.getProperty( property );
-            Value[] values = p.getValues();
-            for( int i = 0; i < values.length; i++ )
-            {
-                if( !values[i].getString().equals( value ) )
-                {
-                    newValues.add( values[i].getString() );
+                    // If page was deleted, remove all references to it/from it
+                case (ContentEvent.NODE_DELETE_REQUEST ): {
+                    WikiPath path = resolvePage( WikiPath.valueOf( pageName ) );
+
+                    // Remove the links from deleted page to its referenced
+                    // pages
+                    removeLinks( path );
+
+                    m_cm.getCurrentSession().save();
+                    break;
+                }
+
+                    // ========= page renamed ==============================
+
+                case (ContentEvent.NODE_RENAMED ): {
+                    // Update references from this page
+                    WikiPath toPage = WikiPath.valueOf( pageName );
+                    WikiPath fromPage = WikiPath.valueOf( (String) ((WikiPageEvent) event).getArgs()[0] );
+                    Boolean changeReferrers = (Boolean) ((WikiPageEvent) event).getArgs()[1];
+                    removeLinks( fromPage );
+                    setLinks( toPage, extractLinks( toPage ) );
+
+                    // Change references to the old page; use the new name
+                    if( changeReferrers )
+                    {
+                        renameLinksTo( fromPage, toPage );
+                    }
+
+                    m_cm.getCurrentSession().save();
+                    break;
                 }
-            }
-            if( newValues.size() == 0 )
-            {
-                // This seems like a hack, but zero-length arrays don't seem to
-                // work
-                // unless we remove the property entirely first.
-                p.remove();
             }
         }
-        catch( PathNotFoundException e )
+        catch( PageNotFoundException e )
         {
-            // No worries
+            e.printStackTrace();
         }
-
-        // Set/remove the property
-        if( newValues.size() > 0 )
+        catch( ProviderException e )
         {
-            node.setProperty( property, newValues.toArray( new String[newValues.size()] ) );
+            e.printStackTrace();
+        }
+        catch( RepositoryException e )
+        {
+            e.printStackTrace();
         }
     }
 
     /**
-     * <p>
-     * Sets links between a WikiPage (source) and a list of pages it links to
-     * (destinations). The source page must exist, but the destination paths
-     * need not. In the source WikiPage, existing outbound <code>refersTo</code>
-     * links for the page are replaced. For all destination pages the page
-     * previously linked to, these pages' inbound <code>referredBy</code>
-     * links are also replaced.
-     * </p>
-     * <p>
-     * Use this method when a new page has been saved, to a) set up its
-     * references and b) notify the referred pages of the references. This
-     * method does not synchronize the database to disk.
-     * </p>
+     * Returns a list of all pages that the ReferenceManager knows about. This
+     * should be roughly equivalent to PageManager.getAllPages(), but without
+     * the potential disk access overhead. Note that this method is not
+     * guaranteed to return a Set of really all pages (especially during
+     * startup), but it is very fast.
      * 
-     * @param source path of the page whose links should be updated
-     * @param destinations the paths the page should link to
+     * @return A Set of all defined page names that ReferenceManager knows
+     *         about.
      * @throws ProviderException
-     * @throws RepositoryException
+     * @since 2.3.24
+     * @deprecated
      */
-    protected synchronized void setLinks( WikiPath source, List<WikiPath> destinations )
-                                                                                        throws ProviderException,
-                                                                                            RepositoryException
+    public Set<String> findCreated() throws ProviderException
     {
-
-        Session s = m_cm.getCurrentSession();
-
-        // Remove all referredBy links from this page
-
-        // First, find all the current outbound links
-        List<WikiPath> oldDestinations = getRefersTo( source );
-        for( WikiPath oldDestination : oldDestinations )
-        {
-            String jcrPath = getReferencedByJCRNode( oldDestination );
-            removeAllFromValues( jcrPath, REFERRED_BY, source.toString() );
-        }
-
-        // Set the new outbound links
-        setRefersTo( source, destinations );
-
-        // Set the new referredBy links
-        for( WikiPath destination : destinations )
+        Collection<WikiPage> c = m_engine.getContentManager().getAllPages( null );
+        Set<String> results = new TreeSet<String>();
+        for( WikiPage link : c )
         {
-            addReferredBy( destination, source );
+            results.add( link.getPath().toString() );
         }
-
-        s.save();
+        return results;
     }
 
     /**
-     * Sets the "refersTo" outbound links between a source page and multiple
-     * destination pages. The source page must exist, although the destination
-     * pages need not.
+     * Returns all pages that this page refers to. You can use this as a quick
+     * way of getting the links from a page, but note that it does not link any
+     * InterWiki, image, or external links. It does contain attachments, though.
+     * <p>
+     * The Collection returned is immutable, so you cannot change it. It does
+     * reflect the current status and thus is a live object. So, if you are
+     * using any kind of an iterator on it, be prepared for
+     * ConcurrentModificationExceptions.
+     * <p>
+     * The returned value is a Collection, because a page may refer to another
+     * page multiple times.
      * 
-     * @param source the page that originates the link
-     * @param destinations the pages that the source page links to
-     * @throws RepositoryException if the underlying JCR node cannot be
-     *             retrieved
+     * @param pageName Page name to query
+     * @return A Collection of Strings containing the names of the pages that
+     *         this page refers to. May return null, if the page does not exist
+     *         or has not been indexed yet.
+     * @throws PageNotFoundException
+     * @throws ProviderException
+     * @since 2.2.33
+     * @deprecated Use {@link #getRefersTo(WikiPath)} instead
      */
-    protected void setRefersTo( WikiPath source, List<WikiPath> destinations ) throws ProviderException, RepositoryException
+    public List<String> findRefersTo( String pageName ) throws ProviderException
     {
-        if( !m_cm.pageExists( source ) )
-        {
-            return;
-        }
-
-        // Transform the destination paths into a String array
-        String[] destinationStrings = new String[destinations.size()];
-        for( int i = 0; i < destinations.size(); i++ )
+        List<WikiPath> links = getRefersTo( WikiPath.valueOf( pageName ) );
+        List<String> results = new ArrayList<String>();
+        for( WikiPath link : links )
         {
-            destinationStrings[i] = destinations.get( i ).toString();
+            results.add( link.toString() );
         }
-
-        // Retrieve the JCR node and add the 'refersTo' links
-        ContentManager cm = m_engine.getContentManager();
-        Node nd = cm.getJCRNode( ContentManager.getJCRPath( source ) );
-        nd.setProperty( REFERS_TO, destinationStrings );
-        nd.save();
+        return results;
     }
 
     /**
      * <p>
-     * Returns a list of Strings representing pages that have been created,
-     * but not yet referenced in wiki markup by any other pages.
-     * Each not-referenced page name is shown only once.
+     * Returns a list of Strings representing pages that are referenced in wiki
+     * markup, but have not yet been created. Each non-existent page name is
+     * shown only once - we don't return information on who referred to it.
      * </p>
      * 
      * @return A list of Strings, where each names a page that hasn't been
      *         created
      */
-    public List<String> findUnreferenced() throws RepositoryException
+    public List<String> findUncreated() throws RepositoryException
     {
-        String[] linkStrings= getValues( REFERENCES_ROOT, NOT_REFERENCED );
+        String[] linkStrings = getFromProperty( NOT_CREATED, PROPERTY_NOT_CREATED );
         List<String> links = new ArrayList<String>();
-        for ( String link : linkStrings )
+        for( String link : linkStrings )
         {
             links.add( link );
         }
@@ -628,19 +476,19 @@
 
     /**
      * <p>
-     * Returns a list of Strings representing pages that are referenced in wiki
-     * markup, but have not yet been created. Each non-existent page name is
-     * shown only once - we don't return information on who referred to it.
+     * Returns a list of Strings representing pages that have been created, but
+     * not yet referenced in wiki markup by any other pages. Each not-referenced
+     * page name is shown only once.
      * </p>
      * 
      * @return A list of Strings, where each names a page that hasn't been
      *         created
      */
-    public List<String> findUncreated() throws RepositoryException
+    public List<String> findUnreferenced() throws RepositoryException
     {
-        String[] linkStrings= getValues( REFERENCES_ROOT, NOT_CREATED );
+        String[] linkStrings = getFromProperty( NOT_REFERENCED, PROPERTY_NOT_REFERENCED );
         List<String> links = new ArrayList<String>();
-        for ( String link : linkStrings )
+        for( String link : linkStrings )
         {
             links.add( link );
         }
@@ -648,36 +496,37 @@
     }
 
     /**
-     * Returns all pages that this page refers to. You can use this as a quick
-     * way of getting the links from a page, but note that it does not link any
-     * InterWiki, image, or external links. It does contain attachments, though.
      * <p>
-     * The Collection returned is immutable, so you cannot change it. It does
-     * reflect the current status and thus is a live object. So, if you are
-     * using any kind of an iterator on it, be prepared for
-     * ConcurrentModificationExceptions.
-     * <p>
-     * The returned value is a Collection, because a page may refer to another
-     * page multiple times.
+     * Returns all pages that refers to a given page. You can use this as a
+     * quick way of getting the inbound links to a page from other pages. The
+     * page being looked up need not exist. The requested page is <code>not</code>
+     * resolved in any way, so if the page is not found as specified exactly by the path,
+     * a zero-length list will be returned.
+     * </p>
      * 
-     * @param pageName Page name to query
-     * @return A Collection of Strings containing the names of the pages that
-     *         this page refers to. May return null, if the page does not exist
-     *         or has not been indexed yet.
-     * @throws PageNotFoundException
-     * @throws ProviderException
-     * @since 2.2.33
-     * @deprecated Use {@link #getRefersTo(String)} instead
+     * @param destination the page to look up
+     * @return the list of pages that link to this page
+     * @throws ProviderException If something goes wrong
+     * @since 3.0
      */
-    public List<String> findRefersTo( String pageName ) throws ProviderException
+    public List<WikiPath> getReferredBy( WikiPath destination ) throws ProviderException
     {
-        List<WikiPath> links = getRefersTo( WikiPath.valueOf( pageName ) );
-        List<String> results = new ArrayList<String>();
-        for( WikiPath link : links )
+        try
         {
-            results.add( link.toString() );
+            String jcrPath = getReferencedByJCRNode( destination );
+            String[] links = getFromProperty( jcrPath, PROPERTY_REFERRED_BY );
+            List<WikiPath> referrers = new ArrayList<WikiPath>();
+            for( String link : links )
+            {
+                referrers.add( WikiPath.valueOf( link ) );
+            }
+            return referrers;
+        }
+        catch( RepositoryException e )
+        {
+            e.printStackTrace();
+            throw new ProviderException( "Could not set 'referredBy' property for " + destination.toString(), e );
         }
-        return results;
     }
 
     /**
@@ -685,9 +534,11 @@
      * Returns all destination pages that a page refers to. You can use this as
      * a quick way of getting the outbound links from a page to the destination
      * pages its markup refers to, but note that it does not link any InterWiki,
-     * image, or external links. It does contain attachments, though. The
-     * requested page is not resolved in any way, so if the page is not found as
-     * specified exactly by the path, a zero-length list will be returned.
+     * image, or external links. It does contain attachments, though. Multiple
+     * links to the same page are never returned; they will always be
+     * de-duplicated. The specified page <code>source</code> is not resolved
+     * in any way, so if the page is not found as specified exactly by the path,
+     * a zero-length list is returned.
      * </p>
      * 
      * @param source the page to look up
@@ -696,196 +547,116 @@
      */
     public List<WikiPath> getRefersTo( WikiPath source ) throws ProviderException
     {
-        ContentManager cm = m_engine.getContentManager();
-        List<WikiPath> links = NO_LINKS;
-
         try
         {
-            links = new ArrayList<WikiPath>();
-
-            Node node = cm.getJCRNode( ContentManager.getJCRPath( source ) );
-            Property p = node.getProperty( REFERS_TO );
-
-            Value[] values = p.getValues();
-
-            for( Value v : values )
+            String jcrPath = ContentManager.getJCRPath( source );
+            String[] links = getFromProperty( jcrPath, PROPERTY_REFERS_TO );
+            List<WikiPath> refersTo = new ArrayList<WikiPath>();
+            for( String link : links )
             {
-                links.add( WikiPath.valueOf( v.getString() ) );
+                refersTo.add( WikiPath.valueOf( link ) );
             }
+            return refersTo;
         }
-        catch( PathNotFoundException e )
+        catch( RepositoryException e )
+        {
+            e.printStackTrace();
+            throw new ProviderException( "Could not get 'refersTo' property for " + source.toString(), e );
+        }
+    }
+
+    /**
+     * Initializes the reference manager. Scans all existing WikiPages for
+     * internal links and adds them to the ReferenceManager object.
+     * 
+     * @param engine The WikiEngine to which this is managing references to.
+     * @param props the properties for initializing the WikiEngine
+     * @throws WikiException If the reference manager initialization fails.
+     */
+    public void initialize( WikiEngine engine, Properties props ) throws WikiException
+    {
+        m_engine = engine;
+        m_cm = engine.getContentManager();
+
+        m_matchEnglishPlurals = TextUtil.getBooleanProperty( engine.getWikiProperties(), WikiEngine.PROP_MATCHPLURALS,
+                                                             m_matchEnglishPlurals );
+
+        m_camelCase = TextUtil.getBooleanProperty( m_engine.getWikiProperties(), JSPWikiMarkupParser.PROP_CAMELCASELINKS, false );
+        try
         {
-            // No worries! Just return the empty list...
+            initReferenceMetadata();
         }
         catch( RepositoryException e )
         {
-            throw new ProviderException( "Unable to get the referrals", e );
+            throw new ProviderException( "Failed to initialize repository contents", e );
         }
-        return links;
+
+        // Make sure we catch any page add/save/rename events
+        WikiEventManager.addWikiEventListener( engine.getContentManager(), this );
+
+        m_cm.release();
+    }
+
+    /**
+     * Rebuilds the internal references database by parsing every wiki page.
+     * 
+     * @throws RepositoryException
+     * @throws LoginException
+     */
+    public void rebuild() throws RepositoryException
+    {
+        // Remove all of the 'referencedBy' inbound links
+        ContentManager cm = m_engine.getContentManager();
+        Session s = cm.getCurrentSession();
+
+        if( s.getRootNode().hasNode( REFERENCES_ROOT ) )
+        {
+            for( String ref : REFERENCES_METADATA )
+            {
+                if( s.getRootNode().hasNode( ref ) )
+                {
+                    Node nd = s.getRootNode().getNode( ref );
+                    nd.remove();
+                }
+            }
+            s.getRootNode().getNode( REFERENCES_ROOT ).remove();
+        }
+        s.save();
+
+        initReferenceMetadata();
+
+        // TODO: we should actually parse the pages
     }
 
     /**
-     * Returns a list of all pages that the ReferenceManager knows about. This
-     * should be roughly equivalent to PageManager.getAllPages(), but without
-     * the potential disk access overhead. Note that this method is not
-     * guaranteed to return a Set of really all pages (especially during
-     * startup), but it is very fast.
-     * 
-     * @return A Set of all defined page names that ReferenceManager knows
-     *         about.
-     * @throws ProviderException
-     * @since 2.3.24
-     * @deprecated
+     * Builds and returns the path used to store the ReferredBy data
      */
-    public Set<String> findCreated() throws ProviderException
+    private String getReferencedByJCRNode( WikiPath name )
     {
-        Set<String> result = new TreeSet<String>();
-        Collection<WikiPage> c = m_engine.getContentManager().getAllPages( null );
-
-        for( WikiPage p : c )
-            result.add( p.getPath().toString() );
-
-        return result;
+        return REFERRED_BY + "/" + name.getSpace() + "/" + name.getPath();
     }
 
     /**
-     * {@inheritDoc} After the page has been saved, updates the reference lists.
+     * Verifies that the JCR nodes for storing references exist, and creates
+     * then if they do not. If any nodes are added, they are saved using the
+     * current JCR {@link Session} before returning.
      */
-    public void actionPerformed( WikiEvent event )
+    private void initReferenceMetadata() throws RepositoryException
     {
-        if( !(event instanceof WikiPageEvent) )
-        {
-            return;
-        }
-
-        String pageName = ((WikiPageEvent) event).getPageName();
-        if( pageName == null )
+        ContentManager cm = m_engine.getContentManager();
+        Session s = cm.getCurrentSession();
+        if( !s.getRootNode().hasNode( REFERENCES_ROOT ) )
         {
-            return;
+            s.getRootNode().addNode( REFERENCES_ROOT );
         }
-
-        try
+        for( String ref : REFERENCES_METADATA )
         {
-            switch( event.getType() )
+            if( !s.getRootNode().hasNode( ref ) )
             {
-                // ========= page deleted ==============================
-                
-                // If page was deleted, remove all references to it/from it
-                case (ContentEvent.NODE_DELETE_REQUEST ): {
-                    WikiPath path = resolvePage( WikiPath.valueOf( pageName ) );
-                    List<WikiPath> referenced = getRefersTo( path );
-                    
-                    // Remove the links from the deleted page to its referenced pages
-                    removeLinks( path );
-                    
-                    // Check each previously-referenced page; see if they still have inbound refs
-                    for ( WikiPath ref : referenced )
-                    {
-                        if ( getReferredBy( ref ).size() == 0 )
-                        {
-                            addToValues( REFERENCES_ROOT, NOT_REFERENCED, pageName, false );
-                        }
-                        else
-                        {
-                            removeAllFromValues( REFERENCES_ROOT, NOT_REFERENCED, pageName );
-                        }
-                    }
-                    
-                    // Remove the deleted page from the 'uncreated' and 'unreferenced' lists
-                    removeAllFromValues( REFERENCES_ROOT, NOT_CREATED, pageName );
-                    removeAllFromValues( REFERENCES_ROOT, NOT_REFERENCED, pageName );
-                    
-                    m_cm.getCurrentSession().save();
-                    
-                    break;
-                }
-
-                // ========= page saved ==============================
-                
-                // If page was saved, update all references
-                case (ContentEvent.NODE_SAVED ): {
-                    WikiPath path = resolvePage( WikiPath.valueOf( pageName ) );
-                    
-                    // Get old linked pages, and add to 'unreferenced list' if needed
-                    List<WikiPath> referenced = extractLinks( path );
-                    for ( WikiPath ref : referenced )
-                    {
-                        ref = resolvePage( ref );
-                        List<WikiPath> referredBy = getReferredBy( ref );
-                        boolean unreferenced = referredBy.size() == 0 || ( referredBy.size() == 1 && referredBy.contains( path ) );
-                        if ( unreferenced )
-                        {
-                            addToValues( REFERENCES_ROOT, NOT_REFERENCED, ref.toString() );
-                        }
-                    }
-                    
-                    // Get the new linked pages, and set refersTo/referencedBy links
-                    referenced = extractLinks( path );
-                    setLinks( path, referenced );
-                    
-                    // Remove the saved page from the 'uncreated' list
-                    removeAllFromValues( REFERENCES_ROOT, NOT_CREATED, pageName );
-                    
-                    // Subtract each link from the 'unreferenced' list; possibly subtract from 'uncreated'
-                    for ( WikiPath ref : referenced )
-                    {
-                        ref = resolvePage( ref );
-                        removeAllFromValues( REFERENCES_ROOT, NOT_REFERENCED, ref.toString() );
-                        if ( m_cm.pageExists( ref ) )
-                        {
-                            removeAllFromValues( REFERENCES_ROOT, NOT_CREATED, ref.toString() );
-                        }
-                        else
-                        {
-                            addToValues( REFERENCES_ROOT, NOT_CREATED, ref.toString(), false );
-                        }
-                    }
-                    
-                    m_cm.getCurrentSession().save();
-                    
-                    break;
-                }
-
-                // ========= page renamed ==============================
-                
-                case (ContentEvent.NODE_RENAMED ): {
-                    // Update references from this page
-                    WikiPath toPage = WikiPath.valueOf( pageName );
-                    WikiPath fromPage = WikiPath.valueOf( (String) ((WikiPageEvent) event).getArgs()[0] );
-                    Boolean changeReferrers = (Boolean) ((WikiPageEvent) event).getArgs()[1];
-                    removeLinks( fromPage );
-                    setLinks( toPage, extractLinks( toPage ) );
-
-                    // Change references to the old page; use the new name
-                    if( changeReferrers )
-                    {
-                        renameLinksTo( fromPage, toPage );
-                    }
-                    
-                    m_cm.getCurrentSession().save();
-                    
-                    break;
-                }
+                s.getRootNode().addNode( ref );
             }
         }
-        catch( PageNotFoundException e )
-        {
-            e.printStackTrace();
-        }
-        catch( ProviderException e )
-        {
-            e.printStackTrace();
-        }
-        catch( RepositoryException e )
-        {
-            e.printStackTrace();
-        }
-    }
-
-    private WikiPath resolvePage( WikiPath path ) throws ProviderException
-    {
-        WikiPath finalPath = m_engine.getFinalPageName( path );
-        return finalPath == null ? path : finalPath;
+        s.save();
     }
 
     /**
@@ -929,6 +700,7 @@
                     // TODO: do we want to set the author here? (We used to...)
                     cm.save( p );
                     setLinks( path, extractLinks( newPath ) );
+                    cm.getCurrentSession().save();
                 }
             }
             catch( PageNotFoundException e )
@@ -939,10 +711,122 @@
     }
 
     /**
+     * Returns a resolved WikiPath, taking into account plural variants as
+     * determined by {@link WikiEngine#getFinalPageName(WikiPath)}. For
+     * example, if page <code>Foobar</code> exists, and the path supplied to
+     * this method is <code>Foobars</code>, then <code>Foobar</code> is
+     * returned. If no variant for the supplied page exists, or if a page exists
+     * whose name exactly matches it, then the supplied name is returned. Thus,
+     * if page <code>Foobars</code> actually exists, that path will be
+     * returned.
+     * 
+     * @param path the path of the page to look for
+     * @return the resolved path for the page
+     * @throws ProviderException if
+     *             {@link WikiEngine#getFinalPageName(WikiPath)} throws an
+     *             exception
+     */
+    private WikiPath resolvePage( WikiPath path ) throws ProviderException
+    {
+        WikiPath finalPath = m_engine.getFinalPageName( path );
+        return finalPath == null ? path : finalPath;
+    }
+
+    /**
+     * Adds a "referredBy" inbound link to a page from a source page that links
+     * to it. That is, for the destination page, a "referredBy" entry is made
+     * that contains the name of the source page. Neither the source or
+     * destination pages need exist. Modifications to the underlying JCR node
+     * that contains the link are saved by the current JCR {@link Session}.
+     * 
+     * @param page the page that is the destination for the link
+     * @param from the page that originates the link
+     * @throws RepositoryException if the underlying JCR node and property
+     *             cannot be retrieved
+     */
+    protected void addReferredBy( WikiPath page, WikiPath from ) throws RepositoryException
+    {
+        // Make sure the 'referredBy' root exists
+        initReferenceMetadata();
+
+        // Set the inverse 'referredBy' link for the destination (referred by
+        // the source)
+        String jcrPath = getReferencedByJCRNode( page );
+        addToProperty( jcrPath, PROPERTY_REFERRED_BY, from.toString(), true );
+    }
+
+    /**
+     * Adds a single String value to a given JCR node and
+     * {@link javax.jcr.Property}. The property is assumed to return an array
+     * of {@link javax.jcr.Value} objects. The node is created if it does not
+     * exist. Modifications to the underlying JCR nodes are saved by the
+     * current JCR {@link Session}.
+     * 
+     * @param jcrNode the JCR path to the node
+     * @param property the property to add to
+     * @param newValue the value to add
+     * @param addAgain whether the value should be added again if it already
+     *            exists in the list
+     */
+    protected void addToProperty( String jcrNode, String property, String newValue, boolean addAgain ) throws RepositoryException
+    {
+        // Retrieve (or create) the destination node for the page
+        ContentManager cm = m_engine.getContentManager();
+        Session s = cm.getCurrentSession();
+        Node node = null;
+        try
+        {
+            node = (Node) cm.getCurrentSession().getItem( jcrNode );
+        }
+        catch( PathNotFoundException e )
+        {
+            if( !s.itemExists( jcrNode ) )
+            {
+                node = cm.createJCRNode( jcrNode );
+            }
+        }
+
+        // Retrieve the property; add value to the end
+        List<String> newValues = new ArrayList<String>();
+        try
+        {
+            boolean notFound = true;
+            Property p = (Property) node.getProperty( property );
+            Value[] values = p.getValues();
+            for( int i = 0; i < values.length; i++ )
+            {
+                newValues.add( values[i].getString() );
+                if( values[i].getString().equals( newValue ) )
+                {
+                    notFound = false;
+                }
+            }
+            if( notFound || addAgain )
+            {
+                newValues.add( newValue );
+            }
+            
+            // There seems to be a bug in Priha that causes property files to bloat,
+            // so we remove the property first, then re-add it
+            p.remove();
+            node.setProperty( property, newValues.toArray( new String[newValues.size()] ) );
+        }
+        catch( PathNotFoundException e )
+        {
+            node.setProperty( property, new String[] { newValue } );
+        }
+        s.save();
+    }
+
+    /**
      * Reads a WikiPage full of data from a String and returns all links
-     * internal to this Wiki in a Collection.
+     * internal to this Wiki in a Collection. Links are "resolved"; that is,
+     * page resolution is performed to ensure that plural references resolve to
+     * the correct page. The links returned by this method will not contain any
+     * duplicates, even if the original page markup linked to the same page more
+     * than once.
      * 
-     * @param page The WikiPage to scan
+     * @param path the of the WikiPage to scan
      * @return a Collection of Strings
      * @throws ProviderException if the page contents cannot be retrieved, or if
      *             MarkupParser canot parse the document
@@ -958,196 +842,334 @@
         mp.addLocalLinkHook( localCollector );
         mp.disableAccessRules();
 
-        // Parse the page, and collect the links
+        // Parse the page, and collect the links
+        try
+        {
+            mp.parse();
+        }
+        catch( IOException e )
+        {
+            // Rethrow any parsing exceptions
+            throw new ProviderException( "Could not parse the document.", e );
+        }
+
+        // Return a WikiPath for each link
+        ArrayList<WikiPath> links = new ArrayList<WikiPath>();
+        for( String s : localCollector.getLinks() )
+        {
+            WikiPath finalPath = resolvePage( WikiPath.valueOf( s ) );
+            if( !links.contains( finalPath ) )
+            {
+                links.add( finalPath );
+            }
+        }
+
+        return links;
+    }
+
+    /**
+     * Retrieves an array of Strings stored at a given JCR node and
+     * {@link javax.jcr.Property}. The property is assumed to return an array
+     * of {@link javax.jcr.Value} objects. If the node does not exist, a
+     * zero-length array is returned.
+     * 
+     * @param jcrNode the JCR path to the node
+     * @param property the property to read
+     * @throws RepositoryException
+     */
+    protected String[] getFromProperty( String jcrNode, String property ) throws RepositoryException
+    {
+        // Retrieve the destination node for the page
+        ContentManager cm = m_engine.getContentManager();
+        Node node = null;
+        try
+        {
+            node = (Node) cm.getCurrentSession().getItem( jcrNode );
+        }
+        catch( PathNotFoundException e )
+        {
+            return NO_VALUES;
+        }
+
+        // Retrieve the property; re-pack value array into String array
+        String[] stringValues = NO_VALUES;
+        try
+        {
+            Property p = (Property) node.getProperty( property );
+            Value[] values = p.getValues();
+            stringValues = new String[values.length];
+            for( int i = 0; i < values.length; i++ )
+            {
+                stringValues[i] = values[i].getString();
+            }
+        }
+        catch( PathNotFoundException e )
+        {
+            return NO_VALUES;
+        }
+
+        return stringValues;
+    }
+
+    /**
+     * Removes a String value from a given JCR node and
+     * {@link javax.jcr.Property}. The property is assumed to return an array
+     * of {@link javax.jcr.Value} objects. The node is created if it does not
+     * exist. Modifications to the underlying JCR node are saved by the
+     * current JCR {@link Session}.
+     * 
+     * @param jcrNode the JCR path to the node
+     * @param property the property to add to
+     * @param value the value to remove. All occurrences of the matching value
+     *            will be removed.
+     */
+    protected void removeFromProperty( String jcrNode, String property, String value ) throws RepositoryException
+    {
+        // Retrieve (or create) the destination node for the page
+        ContentManager cm = m_engine.getContentManager();
+        Session s = cm.getCurrentSession();
+        Node node = null;
+        try
+        {
+            node = (Node) cm.getCurrentSession().getItem( jcrNode );
+        }
+        catch( PathNotFoundException e )
+        {
+            if( !s.itemExists( jcrNode ) )
+            {
+                node = cm.createJCRNode( jcrNode );
+            }
+        }
+
+        // Retrieve the property; remove all instances of value
+        List<String> newValues = new ArrayList<String>();
         try
         {
-            mp.parse();
+            Property p = (Property) node.getProperty( property );
+            Value[] values = p.getValues();
+            for( int i = 0; i < values.length; i++ )
+            {
+                if( !values[i].getString().equals( value ) )
+                {
+                    newValues.add( values[i].getString() );
+                }
+            }
+            if( newValues.size() == 0 )
+            {
+                // This seems like a hack, but zero-length arrays don't seem to
+                // work
+                // unless we remove the property entirely first.
+                p.remove();
+            }
         }
-        catch( IOException e )
+        catch( PathNotFoundException e )
         {
-            // Rethrow any parsing exceptions
-            throw new ProviderException( "Could not parse the document.", e );
+            // No worries
         }
 
-        // Return a WikiPath for each link
-        ArrayList<WikiPath> links = new ArrayList<WikiPath>();
-        for( String s : localCollector.getLinks() )
+        // Set/remove the property
+        if( newValues.size() > 0 )
         {
-            WikiPath finalPath = resolvePage( WikiPath.valueOf( s ) );
-            links.add( finalPath );
+            node.setProperty( property, newValues.toArray( new String[newValues.size()] ) );
         }
-
-        return links;
+        s.save();
     }
 
     /**
-     * Replaces camelcase links.
+     * <p>
+     * Removes all links between a source page and one or more destination
+     * pages, and vice-versa. The source page must exist, although the
+     * destinations may not. Modifications to the underlying JCR nodes are
+     * saved by the current JCR {@link Session}.
+     * </p>
+     * <p>
+     * In addition to setting the inbound and outbound links, this method also
+     * updates the unreferenced/uncreated lists.
+     * </p>
+     * 
+     * @param page Name of the page to remove from the maps.
+     * @throws PageNotFoundException if the source page does not exist
+     * @throws ProviderException
+     * @throws RepositoryException if the links cannot be reset
      */
-    private static String renameCamelCaseLinks( String sourceText, String from, String to )
+    protected void removeLinks( WikiPath page ) throws ProviderException, RepositoryException
     {
-        StringBuilder sb = new StringBuilder( sourceText.length() + 32 );
-
-        Pattern linkPattern = Pattern.compile( "\\p{Lu}+\\p{Ll}+\\p{Lu}+[\\p{L}\\p{Digit}]*" );
-
-        Matcher matcher = linkPattern.matcher( sourceText );
-
-        int start = 0;
-
-        while ( matcher.find( start ) )
-        {
-            String match = matcher.group();
-
-            sb.append( sourceText.substring( start, matcher.start() ) );
-
-            int lastOpenBrace = sourceText.lastIndexOf( '[', matcher.start() );
-            int lastCloseBrace = sourceText.lastIndexOf( ']', matcher.start() );
-
-            if( match.equals( from ) && lastCloseBrace >= lastOpenBrace )
+        // Get old linked pages; add to 'unreferenced list' if needed
+        List<WikiPath> referenced = getRefersTo( page );
+        for( WikiPath ref : referenced )
+        {
+            ref = resolvePage( ref );
+            List<WikiPath> referredBy = getReferredBy( ref );
+
+            // Is 'page' the last inbound link for the destination?
+            boolean unreferenced = referredBy.size() == 0 || (referredBy.size() == 1 && referredBy.contains( page ));
+            if( unreferenced )
             {
-                sb.append( to );
+                addToProperty( NOT_REFERENCED, PROPERTY_NOT_REFERENCED, ref.toString(), false );
             }
             else
             {
-                sb.append( match );
+                removeFromProperty( NOT_REFERENCED, PROPERTY_NOT_REFERENCED, ref.toString() );
             }
+        }
 
-            start = matcher.end();
+        // Remove all inbound links TO the page
+        // Let's pretend B and C ---> A
+
+        // First, remove all inbound links from B & C to A
+        String jcrPath = getReferencedByJCRNode( page );
+        List<WikiPath> inboundLinks = getReferredBy( page );
+        for( WikiPath source : inboundLinks )
+        {
+            removeFromProperty( jcrPath, PROPERTY_REFERRED_BY, source.toString() );
         }
 
-        sb.append( sourceText.substring( start ) );
+        // Remove all outbound links FROM the page
+        // Let's pretend A ---> B and C
 
-        return sb.toString();
+        // Remove all inbound links from B &C to A
+        List<WikiPath> outboundLinks = getRefersTo( page );
+        for( WikiPath destination : outboundLinks )
+        {
+            jcrPath = ContentManager.getJCRPath( page );
+            removeFromProperty( jcrPath, PROPERTY_REFERS_TO, destination.toString() );
+
+            jcrPath = ContentManager.getJCRPath( destination );
+            removeFromProperty( jcrPath, PROPERTY_REFERS_TO, page.toString() );
+
+            jcrPath = getReferencedByJCRNode( destination );
+            removeFromProperty( jcrPath, PROPERTY_REFERRED_BY, page.toString() );
+        }
+
+        // Remove the deleted page from the 'uncreated' and
+        // 'unreferenced' lists
+        removeFromProperty( NOT_CREATED, PROPERTY_NOT_CREATED, page.toString() );
+        removeFromProperty( NOT_REFERENCED, PROPERTY_NOT_REFERENCED, page.toString() );
     }
 
     /**
-     * Renames a link in a given source text into a new name, and returns the
-     * transformed text.
+     * <p>
+     * Sets links between a WikiPage (source) and a list of pages it links to
+     * (destinations). The source page must exist, but the destination paths
+     * need not. In the source WikiPage, existing outbound <code>refersTo</code>
+     * links for the page are replaced. For all destination pages the page
+     * previously linked to, these pages' inbound <code>referredBy</code>
+     * links are also replaced.
+     * </p>
+     * <p>
+     * In addition to setting the inbound and outbound links, this method also
+     * updates the unreferenced/uncreated lists.
+     * </p>
+     * <p>
+     * Use this method when a new page has been saved, to a) set up its
+     * references and b) notify the referred pages of the references.
+     * Modifications to the underlying JCR nodes are not saved by the current
+     * JCR {@link Session}. Callers should call {@link Session#save()} to
+     * ensure any changes are persisted.
+     * </p>
      * 
-     * @param sourceText the source text
-     * @param from the link to change, for example, "Main"
-     * @param to the name to change the link to, for example "RenamedMain"
-     * @return the transformed text
+     * @param source path of the page whose links should be updated
+     * @param destinations the paths the page should link to
+     * @throws ProviderException
+     * @throws RepositoryException
      */
-    protected static String renameLinks( String sourceText, String from, String to )
+    protected void setLinks( WikiPath source, List<WikiPath> destinations ) throws ProviderException, RepositoryException
     {
-        StringBuilder sb = new StringBuilder( sourceText.length() + 32 );
 
-        //
-        // This monstrosity just looks for a JSPWiki link pattern. But it is
-        // pretty
-        // cool for a regexp, isn't it? If you can understand this in a single
-        // reading,
-        // you have way too much time in your hands.
-        //
-        Matcher matcher = LINK_PATTERN.matcher( sourceText );
+        Session s = m_cm.getCurrentSession();
 
-        int start = 0;
+        // Get old linked pages, and add to 'unreferenced list' if needed
+        List<WikiPath> referenced = getRefersTo( source );
+        for( WikiPath ref : referenced )
+        {
+            ref = resolvePage( ref );
+            List<WikiPath> referredBy = getReferredBy( ref );
+            boolean unreferenced = referredBy.size() == 0 || (referredBy.size() == 1 && referredBy.contains( source ));
+            if( unreferenced )
+            {
+                addToProperty( NOT_REFERENCED, PROPERTY_NOT_REFERENCED, ref.toString(), false );
+            }
+        }
 
-        // System.out.println("====");
-        // System.out.println("SRC="+sourceText.trim());
-        while ( matcher.find( start ) )
+        // First, find all the current outbound links
+        List<WikiPath> oldDestinations = getRefersTo( source );
+        for( WikiPath oldDestination : oldDestinations )
         {
-            char charBefore = (char) -1;
+            String jcrPath = getReferencedByJCRNode( oldDestination );
+            removeFromProperty( jcrPath, PROPERTY_REFERRED_BY, source.toString() );
+        }
 
-            if( matcher.start() > 0 )
-                charBefore = sourceText.charAt( matcher.start() - 1 );
+        // Set the new outbound links
+        setRefersTo( source, destinations );
 
-            if( matcher.group( 1 ).length() > 0 || charBefore == '~' || charBefore == '[' )
-            {
-                //
-                // Found an escape character, so I am escaping.
-                //
-                sb.append( sourceText.substring( start, matcher.end() ) );
-                start = matcher.end();
-                continue;
-            }
+        // Set the new referredBy links
+        for( WikiPath destination : destinations )
+        {
+            addReferredBy( destination, source );
+        }
 
-            String text = matcher.group( 2 );
-            String link = matcher.group( 4 );
-            String attr = matcher.group( 6 );
+        // Is the page itself referenced by any other pages?
+        if( getReferredBy( source ).size() == 0 )
+        {
+            addToProperty( NOT_REFERENCED, PROPERTY_NOT_REFERENCED, source.toString(), false );
+        }
+        else
+        {
+            removeFromProperty( NOT_REFERENCED, PROPERTY_NOT_REFERENCED, source.toString() );
+        }
 
-            /*
-             * System.out.println("MATCH="+matcher.group(0));
-             * System.out.println(" text="+text); System.out.println("
-             * link="+link); System.out.println(" attr="+attr);
-             */
-            if( link.length() == 0 )
+        // Subtract each destination link from the 'unreferenced' list; possibly
+        // subtract from 'uncreated'
+        for( WikiPath ref : destinations )
+        {
+            ref = resolvePage( ref );
+            removeFromProperty( NOT_REFERENCED, PROPERTY_NOT_REFERENCED, ref.toString() );
+            if( m_cm.pageExists( ref ) )
             {
-                text = renameLink( text, from, to );
+                removeFromProperty( NOT_CREATED, PROPERTY_NOT_CREATED, ref.toString() );
             }
             else
             {
-                link = renameLink( link, from, to );
-
-                //
-                // A very simple substitution, but should work for quite a few
-                // cases.
-                //
-                text = TextUtil.replaceString( text, from, to );
+                addToProperty( NOT_CREATED, PROPERTY_NOT_CREATED, ref.toString(), false );
             }
-
-            //
-            // Construct the new string
-            //
-            sb.append( sourceText.substring( start, matcher.start() ) );
-            sb.append( "[" + text );
-            if( link.length() > 0 )
-                sb.append( "|" + link );
-            if( attr.length() > 0 )
-                sb.append( "|" + attr );
-            sb.append( "]" );
-
-            start = matcher.end();
         }
 
-        sb.append( sourceText.substring( start ) );
+        // Remove the saved page from the 'uncreated' list
+        removeFromProperty( NOT_CREATED, PROPERTY_NOT_CREATED, source.toString() );
 
-        return sb.toString();
+        s.save();
     }
 
     /**
-     * This method does a correct replacement of a single link, taking into
-     * account anchors and attachments.
+     * Sets the "refersTo" outbound links between a source page and multiple
+     * destination pages. The source page must exist, although the destination
+     * pages need not. Modifications to the underlying JCR nodes are <em>not</em> saved
+     * by the current JCR {@link Session}. Callers should call
+     * {@link Session#save()} to ensure any changes are persisted.
+     * 
+     * @param source the page that originates the link
+     * @param destinations the pages that the source page links to. These are
+     *            expected to have been previously resolved
+     * @throws RepositoryException if the underlying JCR node cannot be
+     *             retrieved
      */
-    private static String renameLink( String original, String from, String newlink )
+    protected void setRefersTo( WikiPath source, List<WikiPath> destinations ) throws ProviderException, RepositoryException
     {
-        int hash = original.indexOf( '#' );
-        int slash = original.indexOf( '/' );
-        String reallink = original;
-        String oldStyleRealLink;
-
-        if( hash != -1 )
-            reallink = original.substring( 0, hash );
-        if( slash != -1 )
-            reallink = original.substring( 0, slash );
-
-        reallink = MarkupParser.cleanLink( reallink );
-        oldStyleRealLink = MarkupParser.wikifyLink( reallink );
-
-        // WikiPage realPage = context.getEngine().getPage( reallink );
-        // WikiPage p2 = context.getEngine().getPage( from );
-
-        // System.out.println(" "+reallink+" :: "+ from);
-        // System.out.println(" "+p+" :: "+p2);
-
-        //
-        // Yes, these point to the same page.
-        //
-        if( reallink.equals( from ) || original.equals( from ) || oldStyleRealLink.equals( from ) )
+        if( !m_cm.pageExists( source ) )
         {
-            //
-            // if the original contains blanks, then we should introduce a link,
-            // for example: [My Page] => [My Page|My Renamed Page]
-            int blank = reallink.indexOf( " " );
-
-            if( blank != -1 )
-            {
-                return original + "|" + newlink;
-            }
+            return;
+        }
 
-            return newlink + ((hash > 0) ? original.substring( hash ) : "") + ((slash > 0) ? original.substring( slash ) : "");
+        // Transform the destination paths into a String array
+        String[] destinationStrings = new String[destinations.size()];
+        for( int i = 0; i < destinations.size(); i++ )
+        {
+            destinationStrings[i] = destinations.get( i ).toString();
         }
 
-        return original;
+        // Retrieve the JCR node and add the 'refersTo' links
+        ContentManager cm = m_engine.getContentManager();
+        Node nd = cm.getJCRNode( ContentManager.getJCRPath( source ) );
+        nd.setProperty( PROPERTY_REFERS_TO, destinationStrings );
     }
 }

Modified: incubator/jspwiki/trunk/src/java/org/apache/wiki/Release.java
URL: http://svn.apache.org/viewvc/incubator/jspwiki/trunk/src/java/org/apache/wiki/Release.java?rev=771072&r1=771071&r2=771072&view=diff
==============================================================================
--- incubator/jspwiki/trunk/src/java/org/apache/wiki/Release.java (original)
+++ incubator/jspwiki/trunk/src/java/org/apache/wiki/Release.java Sun May  3 16:04:00 2009
@@ -77,7 +77,7 @@
      *  <p>
      *  If the build identifier is empty, it is not added.
      */
-    public static final String     BUILD         = "108";
+    public static final String     BUILD         = "109";
     
     /**
      *  This is the generic version string you should use



Mime
View raw message