You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@jspwiki.apache.org by aj...@apache.org on 2008/02/13 06:54:24 UTC

svn commit: r627255 [3/41] - in /incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src: ./ com/ com/ecyrd/ com/ecyrd/jspwiki/ com/ecyrd/jspwiki/action/ com/ecyrd/jspwiki/attachment/ com/ecyrd/jspwiki/auth/ com/ecyrd/jspwiki/auth/acl/ com/ecyrd/jspwiki/...

Added: incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/ReferenceManager.java
URL: http://svn.apache.org/viewvc/incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/ReferenceManager.java?rev=627255&view=auto
==============================================================================
--- incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/ReferenceManager.java (added)
+++ incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/ReferenceManager.java Tue Feb 12 21:53:55 2008
@@ -0,0 +1,1133 @@
+/*
+    JSPWiki - a JSP-based WikiWiki clone.
+
+    Copyright (C) 2001-2004 Janne Jalkanen (Janne.Jalkanen@iki.fi),
+                            Erik Bunn (ebu@memecry.net)
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU Lesser General Public License as published by
+    the Free Software Foundation; either version 2.1 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Lesser General Public License for more details.
+
+    You should have received a copy of the GNU Lesser General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+package com.ecyrd.jspwiki;
+
+import java.io.*;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.*;
+
+import org.apache.commons.lang.time.StopWatch;
+import org.apache.log4j.Logger;
+
+import com.ecyrd.jspwiki.attachment.Attachment;
+import com.ecyrd.jspwiki.event.WikiEvent;
+import com.ecyrd.jspwiki.event.WikiEventListener;
+import com.ecyrd.jspwiki.event.WikiEventUtils;
+import com.ecyrd.jspwiki.event.WikiPageEvent;
+import com.ecyrd.jspwiki.filters.BasicPageFilter;
+import com.ecyrd.jspwiki.modules.InternalModule;
+import com.ecyrd.jspwiki.providers.ProviderException;
+import com.ecyrd.jspwiki.providers.WikiPageProvider;
+
+/*
+  BUGS
+
+  - if a wikilink is added to a page, then removed, RefMan still thinks that
+    the page refers to the wikilink page. Hm.
+
+  - if a page is deleted, gets very confused.
+
+  - Serialization causes page attributes to be missing, when InitializablePlugins
+    are not executed properly.  Thus, serialization should really also mark whether
+    a page is serializable or not...
+ */
+
+
+/*
+   A word about synchronizing:
+
+   I expect this object to be accessed in three situations:
+   - when a WikiEngine is created and it scans its wikipages
+   - when the WE saves a page
+   - when a JSP page accesses one of the WE's ReferenceManagers
+     to display a list of (un)referenced pages.
+
+   So, access to this class is fairly rare, and usually triggered by
+   user interaction. OTOH, the methods in this class use their storage
+   objects intensively (and, sorry to say, in an unoptimized manner =).
+   My deduction: using unsynchronized HashMaps etc and syncing methods
+   or code blocks is preferrable to using slow, synced storage objects.
+   We don't have iterative code here, so I'm going to use synced methods
+   for now.
+
+   Please contact me if you notice problems with ReferenceManager, and
+   especially with synchronization, or if you have suggestions about
+   syncing.
+
+   ebu@memecry.net
+*/
+
+/**
+ *  Keeps track of wikipage references:
+ *  <UL>
+ *  <LI>What pages a given page refers to
+ *  <LI>What pages refer to a given page
+ *  </UL>
+ *
+ *  This is a quick'n'dirty approach without any finesse in storage and
+ *  searching algorithms; we trust java.util.*.
+ *  <P>
+ *  This class contains two HashMaps, m_refersTo and m_referredBy. The
+ *  first is indexed by WikiPage names and contains a Collection of all
+ *  WikiPages the page refers to. (Multiple references are not counted,
+ *  naturally.) The second is indexed by WikiPage names and contains
+ *  a Set of all pages that refer to the indexing page. (Notice -
+ *  the keys of both Maps should be kept in sync.)
+ *  <P>
+ *  When a page is added or edited, its references are parsed, a Collection
+ *  is received, and we crudely replace anything previous with this new
+ *  Collection. We then check each referenced page name and make sure they
+ *  know they are referred to by the new page.
+ *  <P>
+ *  Based on this information, we can perform non-optimal searches for
+ *  e.g. unreferenced pages, top ten lists, etc.
+ *  <P>
+ *  The owning class must take responsibility of filling in any pre-existing
+ *  information, probably by loading each and every WikiPage and calling this
+ *  class to update the references when created.
+ *
+ *  @author ebu@memecry.net
+ *  @since 1.6.1
+ */
+
+// FIXME: The way that we save attributes is now a major booboo, and must be
+//        replace forthwith.  However, this is a workaround for the great deal
+//        of problems that occur here...
+
+public class ReferenceManager
+    extends BasicPageFilter
+    implements InternalModule, WikiEventListener
+{
+    /** Maps page wikiname to a Collection of pages it refers to. The Collection
+     *  must contain Strings. The Collection may contain names of non-existing
+     *  pages.
+     */
+    private Map<String,Set<String>> m_refersTo;
+    private Map<String,Set<String>> m_unmutableRefersTo;
+
+    /** Maps page wikiname to a Set of referring pages. The Set must
+     *  contain Strings. Non-existing pages (a reference exists, but not a file
+     *  for the page contents) may have an empty Set in m_referredBy.
+     */
+    private Map<String,Set<String>> m_referredBy;
+    private Map<String,Set<String>> m_unmutableReferredBy;
+
+    /** The WikiEngine that owns this object. */
+    private WikiEngine     m_engine;
+
+    private boolean        m_matchEnglishPlurals = false;
+
+    private static Logger log = Logger.getLogger(ReferenceManager.class);
+
+    private static final String SERIALIZATION_FILE = "refmgr.ser";
+    private static final String SERIALIZATION_DIR  = "refmgr-attr";
+
+    /** We use this also a generic serialization id */
+    private static final long serialVersionUID = 2L;
+
+    /**
+     *  Builds a new ReferenceManager.
+     *
+     *  @param engine The WikiEngine to which this is managing references to.
+     */
+    public ReferenceManager( WikiEngine engine )
+    {
+        m_refersTo   = new HashMap<String,Set<String>>();
+        m_referredBy = new HashMap<String,Set<String>>();
+        m_engine = engine;
+
+        m_matchEnglishPlurals = TextUtil.getBooleanProperty( engine.getWikiProperties(),
+                                                             WikiEngine.PROP_MATCHPLURALS,
+                                                             m_matchEnglishPlurals );
+
+        //
+        //  Create two maps that contain unmutable versions of the two basic maps.
+        //
+        m_unmutableReferredBy = Collections.unmodifiableMap( m_referredBy );
+        m_unmutableRefersTo   = Collections.unmodifiableMap( m_refersTo );
+    }
+
+    /**
+     *  Does a full reference update.  Does not sync; assumes that you do it afterwards.
+     */
+    private void updatePageReferences( WikiPage page )
+        throws ProviderException
+    {
+        String content = m_engine.getPageManager().getPageText( page.getName(),
+                                                                WikiPageProvider.LATEST_VERSION );
+
+        TreeSet<String> res = new TreeSet<String>();
+        Collection<String> links = m_engine.scanWikiLinks( page, content );
+
+        res.addAll( links );
+        Collection<Attachment> attachments = m_engine.getAttachmentManager().listAttachments( page );
+
+        for( Attachment atti : attachments )
+        {
+            res.add( atti.getName() );
+        }
+
+        internalUpdateReferences( page.getName(), res );
+    }
+
+    /**
+     *  Initializes the entire reference manager with the initial set of pages
+     *  from the collection.
+     *
+     *  @param pages A collection of all pages you want to be included in the reference
+     *               count.
+     *  @since 2.2
+     */
+    public void initialize( Collection<WikiPage> pages )
+        throws ProviderException
+    {
+        log.debug( "Initializing new ReferenceManager with "+pages.size()+" initial pages." );
+        StopWatch sw = new StopWatch();
+        sw.start();
+        log.info( "Starting cross reference scan of WikiPages" );
+
+        //
+        //  First, try to serialize old data from disk.  If that fails,
+        //  we'll go and update the entire reference lists (which'll take
+        //  time)
+        //
+        try
+        {
+            //
+            //  Unserialize things.  The loop below cannot be combined with
+            //  the other loop below, simply because engine.getPage() has
+            //  side effects such as loading initializing the user databases,
+            //  which in turn want all of the pages to be read already...
+            //
+            //  Yes, this is a kludge.  We know.  Will be fixed.
+            //
+            long saved = unserializeFromDisk();
+
+            for( WikiPage page : pages )
+            {
+                unserializeAttrsFromDisk( page );
+            }
+
+            //
+            //  Now we must check if any of the pages have been changed
+            //  while we were in the electronic la-la-land, and update
+            //  the references for them.
+            //
+
+            Iterator<WikiPage> it = pages.iterator();
+
+            while( it.hasNext() )
+            {
+                WikiPage page = it.next();
+
+                if( page instanceof Attachment )
+                {
+                    // Skip attachments
+                }
+                else
+                {
+
+                    // Refresh with the latest copy
+                    page = m_engine.getPage( page.getName() );
+
+                    if( page.getLastModified() == null )
+                    {
+                        log.fatal( "Provider returns null lastModified.  Please submit a bug report." );
+                    }
+                    else if( page.getLastModified().getTime() > saved )
+                    {
+                        updatePageReferences( page );
+                    }
+                }
+            }
+
+        }
+        catch( Exception e )
+        {
+            log.info("Unable to unserialize old refmgr information, rebuilding database: "+e.getMessage());
+            buildKeyLists( pages );
+
+            // Scan the existing pages from disk and update references in the manager.
+            Iterator<WikiPage> it = pages.iterator();
+            while( it.hasNext() )
+            {
+                WikiPage page  = it.next();
+
+                if( page instanceof Attachment )
+                {
+                    // We cannot build a reference list from the contents
+                    // of attachments, so we skip them.
+                }
+                else
+                {
+                    updatePageReferences( page );
+
+                    serializeAttrsToDisk( page );
+                }
+
+            }
+
+            serializeToDisk();
+        }
+
+        sw.stop();
+        log.info( "Cross reference scan done in "+sw );
+
+        WikiEventUtils.addWikiEventListener(m_engine.getPageManager(),
+                                            WikiPageEvent.PAGE_DELETED, this);
+    }
+
+    /**
+     *  Reads the serialized data from the disk back to memory.
+     *  Returns the date when the data was last written on disk
+     */
+    private synchronized long unserializeFromDisk()
+        throws IOException,
+               ClassNotFoundException
+    {
+        ObjectInputStream in = null;
+        long saved = 0L;
+
+        try
+        {
+            StopWatch sw = new StopWatch();
+            sw.start();
+
+            File f = new File( m_engine.getWorkDir(), SERIALIZATION_FILE );
+
+            in = new ObjectInputStream( new BufferedInputStream(new FileInputStream(f)) );
+
+            long ver     = in.readLong();
+
+            if( ver != serialVersionUID )
+            {
+                throw new IOException("File format has changed; I need to recalculate references.");
+            }
+
+            saved        = in.readLong();
+            m_refersTo   = (Map<String,Set<String>>) in.readObject();
+            m_referredBy = (Map<String,Set<String>>) in.readObject();
+
+            in.close();
+
+            m_unmutableReferredBy = Collections.unmodifiableMap( m_referredBy );
+            m_unmutableRefersTo   = Collections.unmodifiableMap( m_refersTo );
+
+            sw.stop();
+            log.debug("Read serialized data successfully in "+sw);
+        }
+        finally
+        {
+            try
+            {
+                if( in != null ) in.close();
+            }
+            catch( IOException ex ) {}
+        }
+
+        return saved;
+    }
+
+    /**
+     *  Serializes hashmaps to disk.  The format is private, don't touch it.
+     */
+    private synchronized void serializeToDisk()
+    {
+        ObjectOutputStream out = null;
+
+        try
+        {
+            StopWatch sw = new StopWatch();
+            sw.start();
+
+            File f = new File( m_engine.getWorkDir(), SERIALIZATION_FILE );
+
+            out = new ObjectOutputStream( new BufferedOutputStream(new FileOutputStream(f)) );
+
+            out.writeLong( serialVersionUID );
+            out.writeLong( System.currentTimeMillis() ); // Timestamp
+            out.writeObject( m_refersTo );
+            out.writeObject( m_referredBy );
+
+            out.close();
+
+            sw.stop();
+
+            log.debug("serialization done - took "+sw);
+        }
+        catch( IOException e )
+        {
+            log.error("Unable to serialize!");
+
+            try
+            {
+                if( out != null ) out.close();
+            }
+            catch( IOException ex ) {}
+        }
+    }
+
+    private String getHashFileName( String pageName )
+        throws NoSuchAlgorithmException
+    {
+        MessageDigest digest = MessageDigest.getInstance("MD5");
+
+        byte[] dig;
+        try
+        {
+            dig = digest.digest( pageName.getBytes("UTF-8") );
+        }
+        catch (UnsupportedEncodingException e)
+        {
+            throw new InternalWikiException("AAAAGH!  UTF-8 is gone!  My eyes!  It burns...!");
+        }
+
+        return TextUtil.toHexString(dig)+".cache";
+    }
+
+    /**
+     *  Reads the serialized data from the disk back to memory.
+     *  Returns the date when the data was last written on disk
+     */
+    private synchronized long unserializeAttrsFromDisk(WikiPage p)
+        throws IOException,
+               ClassNotFoundException
+    {
+        ObjectInputStream in = null;
+        long saved = 0L;
+
+        try
+        {
+            StopWatch sw = new StopWatch();
+            sw.start();
+
+            //
+            //  Find attribute cache, and check if it exists
+            //
+            File f = new File( m_engine.getWorkDir(), SERIALIZATION_DIR );
+
+            f = new File( f, getHashFileName(p.getName()) );
+
+            if( !f.exists() )
+            {
+                return 0L;
+            }
+
+            log.debug("Deserializing attributes for "+p.getName());
+
+            in = new ObjectInputStream( new BufferedInputStream(new FileInputStream(f)) );
+
+            long ver     = in.readLong();
+
+            if( ver != serialVersionUID )
+            {
+                log.debug("File format has changed; cannot deserialize.");
+                return 0L;
+            }
+
+            saved        = in.readLong();
+
+            String name  = in.readUTF();
+
+            if( !name.equals(p.getName()) )
+            {
+                log.debug("File name does not match ("+name+"), skipping...");
+                return 0L; // Not here
+            }
+
+            long entries = in.readLong();
+
+            for( int i = 0; i < entries; i++ )
+            {
+                String key   = in.readUTF();
+                Object value = in.readObject();
+
+                p.setAttribute( key, value );
+
+                log.debug("   attr: "+key+"="+value);
+            }
+
+            in.close();
+
+            sw.stop();
+            log.debug("Read serialized data for "+name+" successfully in "+sw);
+        }
+        catch( NoSuchAlgorithmException e )
+        {
+            log.fatal("No MD5!?!");
+        }
+        finally
+        {
+            try
+            {
+                if( in != null ) in.close();
+            }
+            catch( IOException ex ) {}
+        }
+
+        return saved;
+    }
+
+    /**
+     *  Serializes hashmaps to disk.  The format is private, don't touch it.
+     */
+    private synchronized void serializeAttrsToDisk( WikiPage p )
+    {
+        ObjectOutputStream out = null;
+
+        try
+        {
+            // FIXME: There is a concurrency issue here...
+            Set entries = p.getAttributes().entrySet();
+
+            if( entries.size() == 0 ) return;
+
+            StopWatch sw = new StopWatch();
+            sw.start();
+
+            File f = new File( m_engine.getWorkDir(), SERIALIZATION_DIR );
+
+            if( !f.exists() ) f.mkdirs();
+
+            //
+            //  Create a digest for the name
+            //
+            f = new File( f, getHashFileName(p.getName()) );
+
+            out = new ObjectOutputStream( new BufferedOutputStream(new FileOutputStream(f)) );
+
+            out.writeLong( serialVersionUID );
+            out.writeLong( System.currentTimeMillis() ); // Timestamp
+
+            out.writeUTF( p.getName() );
+            out.writeLong( entries.size() );
+
+            for( Iterator i = entries.iterator(); i.hasNext(); )
+            {
+                Map.Entry e = (Map.Entry) i.next();
+
+                if( e.getValue() instanceof Serializable )
+                {
+                    out.writeUTF( (String)e.getKey() );
+                    out.writeObject( e.getValue() );
+                }
+            }
+
+            out.close();
+
+            sw.stop();
+
+            log.debug("serialization for "+p.getName()+" done - took "+sw);
+        }
+        catch( IOException e )
+        {
+            log.error("Unable to serialize!");
+
+            try
+            {
+                if( out != null ) out.close();
+            }
+            catch( IOException ex ) {}
+        }
+        catch( NoSuchAlgorithmException e )
+        {
+            log.fatal("No MD5 algorithm!?!");
+        }
+    }
+
+    /**
+     *  After the page has been saved, updates the reference lists.
+     */
+    public void postSave( WikiContext context, String content )
+    {
+        WikiPage page = context.getPage();
+
+        updateReferences( page.getName(),
+                          context.getEngine().scanWikiLinks( page, content ) );
+
+        serializeAttrsToDisk( page );
+    }
+
+    /**
+     * Updates the m_referedTo and m_referredBy hashmaps when a page has been
+     * deleted.
+     * <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.
+     *
+     *  @param page Name of the page to remove from the maps.
+     */
+    public synchronized void pageRemoved( WikiPage page )
+    {
+        String pageName = page.getName();
+
+        pageRemoved(pageName);
+    }
+
+    private void pageRemoved(String pageName)
+    {
+        Collection<String> refTo = m_refersTo.get( pageName );
+
+        if( refTo != null )
+        {
+            Iterator it_refTo = refTo.iterator();
+            while( it_refTo.hasNext() )
+            {
+                String referredPageName = (String)it_refTo.next();
+                Set<String> refBy = m_referredBy.get( referredPageName );
+
+                if( refBy == null )
+                    throw new InternalWikiException("Refmgr out of sync: page "+pageName+" refers to "+referredPageName+", which has null referrers.");
+
+                refBy.remove(pageName);
+
+                m_referredBy.remove( referredPageName );
+
+                // We won't put it back again if it becomes empty and does not exist.  It will be added
+                // later on anyway, if it becomes referenced again.
+                if( !(refBy.isEmpty() && !m_engine.pageExists(referredPageName)) )
+                {
+                    m_referredBy.put( referredPageName, refBy );
+                }
+            }
+
+            log.debug("Removing from m_refersTo HashMap key:value "+pageName+":"+m_refersTo.get( pageName ));
+            m_refersTo.remove( pageName );
+        }
+
+        Set<String> refBy = m_referredBy.get( pageName );
+        if( refBy == null || refBy.isEmpty() )
+        {
+            m_referredBy.remove( pageName );
+        }
+
+        //
+        //  Remove any traces from the disk, too
+        //
+        serializeToDisk();
+
+        try
+        {
+            File f = new File( m_engine.getWorkDir(), SERIALIZATION_DIR );
+
+            f = new File( f, getHashFileName(pageName) );
+
+            if( f.exists() ) f.delete();
+        }
+        catch( NoSuchAlgorithmException e )
+        {
+            log.error("What do you mean - no such algorithm?", e);
+        }
+    }
+
+    /**
+     *  Updates the referred pages of a new or edited WikiPage. If a refersTo
+     *  entry for this page already exists, it is removed and a new one is built
+     *  from scratch. Also calls updateReferredBy() for each referenced page.
+     *  <P>
+     *  This is the method to call when a new page has been created and we
+     *  want to a) set up its references and b) notify the referred pages
+     *  of the references. Use this method during run-time.
+     *
+     *  @param page Name of the page to update.
+     *  @param references A Collection of Strings, each one pointing to a page this page references.
+     */
+    public synchronized void updateReferences( String page, Collection<String> references )
+    {
+        internalUpdateReferences(page, references);
+
+        serializeToDisk();
+    }
+
+    /**
+     *  Updates the referred pages of a new or edited WikiPage. If a refersTo
+     *  entry for this page already exists, it is removed and a new one is built
+     *  from scratch. Also calls updateReferredBy() for each referenced page.
+     *  <p>
+     *  This method does not synchronize the database to disk.
+     *
+     *  @param page Name of the page to update.
+     *  @param references A Collection of Strings, each one pointing to a page this page references.
+     */
+
+    private void internalUpdateReferences(String page, Collection<String> references)
+    {
+        page = getFinalPageName( page );
+
+        //
+        // Create a new entry in m_refersTo.
+        //
+        Collection<String> oldRefTo = m_refersTo.get( page );
+        m_refersTo.remove( page );
+
+        TreeSet<String> cleanedRefs = new TreeSet<String>();
+        for( String ref : references )
+        {
+            ref = getFinalPageName( ref );
+
+            cleanedRefs.add( ref );
+        }
+
+        m_refersTo.put( page, cleanedRefs );
+
+        //
+        //  We know the page exists, since it's making references somewhere.
+        //  If an entry for it didn't exist previously in m_referredBy, make
+        //  sure one is added now.
+        //
+        if( !m_referredBy.containsKey( page ) )
+        {
+            m_referredBy.put( page, new TreeSet<String>() );
+        }
+
+        //
+        //  Get all pages that used to be referred to by 'page' and
+        //  remove that reference. (We don't want to try to figure out
+        //  which particular references were removed...)
+        //
+        cleanReferredBy( page, oldRefTo, cleanedRefs );
+
+        //
+        //  Notify all referred pages of their referinesshoodicity.
+        //
+        for( String referredPageName : cleanedRefs )
+        {
+            updateReferredBy( getFinalPageName(referredPageName), page );
+        }
+    }
+
+    /**
+     * Returns the refers-to list. For debugging.
+     */
+    protected Map getRefersTo()
+    {
+        return m_refersTo;
+    }
+
+    /**
+     * Returns the referred-by list. For debugging.
+     */
+    protected Map getReferredBy()
+    {
+        return m_referredBy;
+    }
+
+    /**
+     * Cleans the 'referred by' list, removing references by 'referrer' to
+     * any other page. Called after 'referrer' is removed.
+     */
+    private void cleanReferredBy( String referrer, 
+                                  Collection<String> oldReferred,
+                                  Collection<String> newReferred )
+    {
+        // Two ways to go about this. One is to look up all pages previously
+        // referred by referrer and remove referrer from their lists, and let
+        // the update put them back in (except possibly removed ones).
+        // The other is to get the old referred to list, compare to the new,
+        // and tell the ones missing in the latter to remove referrer from
+        // their list. Hm. We'll just try the first for now. Need to come
+        // back and optimize this a bit.
+
+        if( oldReferred == null )
+            return;
+
+        for( String referredPage : oldReferred )
+        {
+            Set<String> oldRefBy = m_referredBy.get( referredPage );
+            if( oldRefBy != null )
+            {
+                oldRefBy.remove( referrer );
+            }
+
+            // If the page is referred to by no one AND it doesn't even
+            // exist, we might just as well forget about this entry.
+            // It will be added again elsewhere if new references appear.
+            if( ( ( oldRefBy == null ) || ( oldRefBy.isEmpty() ) ) &&
+                ( m_engine.pageExists( referredPage ) == false ) )
+            {
+                m_referredBy.remove( referredPage );
+            }
+        }
+
+    }
+
+
+    /**
+     *  When initially building a ReferenceManager from scratch, call this method
+     * BEFORE calling updateReferences() with a full list of existing page names.
+     * It builds the refersTo and referredBy key lists, thus enabling
+     * updateReferences() to function correctly.
+     * <P>
+     * This method should NEVER be called after initialization. It clears all mappings
+     * from the reference tables.
+     *
+     * @param pages   a Collection containing WikiPage objects.
+     */
+    private synchronized void buildKeyLists( Collection<WikiPage> pages )
+    {
+        m_refersTo.clear();
+        m_referredBy.clear();
+
+        if( pages == null )
+            return;
+
+        try
+        {
+            for( WikiPage page : pages )
+            {
+                // We add a non-null entry to referredBy to indicate the referred page exists
+                m_referredBy.put( page.getName(), new TreeSet<String>() );
+                // Just add a key to refersTo; the keys need to be in sync with referredBy.
+                m_refersTo.put( page.getName(), (TreeSet<String>)null );
+            }
+        }
+        catch( ClassCastException e )
+        {
+            log.fatal( "Invalid collection entry in ReferenceManager.buildKeyLists().", e );
+        }
+    }
+
+
+    /**
+     * Marks the page as referred to by the referrer. If the page does not
+     * exist previously, nothing is done. (This means that some page, somewhere,
+     * has a link to a page that does not exist.)
+     * <P>
+     * This method is NOT synchronized. It should only be referred to from
+     * within a synchronized method, or it should be made synced if necessary.
+     */
+    private void updateReferredBy( String page, String referrer )
+    {
+        // We're not really interested in first level self-references.
+        if( page.equals( referrer ) )
+        {
+            return;
+        }
+
+        // Neither are we interested if plural forms refer to each other.
+        if( m_matchEnglishPlurals )
+        {
+            String p2 = page.endsWith("s") ? page.substring(0,page.length()-1) : page+"s";
+
+            if( referrer.equals(p2) )
+            {
+                return;
+            }
+        }
+
+        Set<String> referrers = m_referredBy.get( page );
+
+        // Even if 'page' has not been created yet, it can still be referenced.
+        // This requires we don't use m_referredBy keys when looking up missing
+        // pages, of course.
+        if(referrers == null)
+        {
+            referrers = new TreeSet<String>();
+            m_referredBy.put( page, referrers );
+        }
+        referrers.add( referrer );
+    }
+
+
+    /**
+     * Clears the references to a certain page so it's no longer in the map.
+     *
+     * @param pagename  Name of the page to clear references for.
+     */
+    public synchronized void clearPageEntries( String pagename )
+    {
+        pagename = getFinalPageName(pagename);
+
+        //
+        //  Remove this item from the referredBy list of any page
+        //  which this item refers to.
+        //
+        Collection<String> c = m_refersTo.get( pagename );
+
+        if( c != null )
+        {
+            for( Iterator<String> i = c.iterator(); i.hasNext(); )
+            {
+                Collection<String> dref = m_referredBy.get( i.next() );
+
+                dref.remove( pagename );
+            }
+        }
+
+        //
+        //  Finally, remove direct references.
+        //
+        m_referredBy.remove( pagename );
+        m_refersTo.remove( pagename );
+    }
+
+
+    /**
+     *  Finds all unreferenced pages. This requires a linear scan through
+     *  m_referredBy to locate keys with null or empty values.
+     */
+    public synchronized Collection<String> findUnreferenced()
+    {
+        List<String> unref = new ArrayList<String>();
+
+        Iterator<String> it = m_referredBy.keySet().iterator();
+
+        while( it.hasNext() )
+        {
+            String key = it.next();
+            //Set refs = (Set) m_referredBy.get( key );
+            Set refs = getReferenceList( m_referredBy, key );
+            if( refs == null || refs.isEmpty() )
+            {
+                unref.add( key );
+            }
+        }
+
+        return unref;
+    }
+
+
+    /**
+     * Finds all references to non-existant pages. This requires a linear
+     * scan through m_refersTo values; each value must have a corresponding
+     * key entry in the reference Maps, otherwise such a page has never
+     * been created.
+     * <P>
+     * Returns a Collection containing Strings of unreferenced page names.
+     * Each non-existant page name is shown only once - we don't return information
+     * on who referred to it.
+     */
+    public synchronized Collection<String> findUncreated()
+    {
+        Set<String> uncreated = new TreeSet<String>();
+
+        // Go through m_refersTo values and check that m_refersTo has the corresponding keys.
+        // We want to reread the code to make sure our HashMaps are in sync...
+
+        for( Collection<String> refs : m_refersTo.values())
+        {
+            if( refs != null )
+            {
+                for ( String aReference : refs )
+                {
+                    if( m_engine.pageExists( aReference ) == false )
+                    {
+                        uncreated.add( aReference );
+                    }
+                }
+            }
+        }
+
+        return uncreated;
+    }
+
+    /**
+     *  Searches for the given page in the given Map.
+     */
+    private Set<String> getReferenceList( Map<String,Set<String>> coll, String pagename )
+    {
+        Set<String> refs = coll.get( pagename );
+
+        if( m_matchEnglishPlurals )
+        {
+            //
+            //  We'll add also matches from the "other" page.
+            //
+            Set<String> refs2;
+
+            if( pagename.endsWith("s") )
+            {
+                refs2 = coll.get( pagename.substring(0,pagename.length()-1) );
+            }
+            else
+            {
+                refs2 = coll.get( pagename+"s" );
+            }
+
+            if( refs2 != null )
+            {
+                if( refs != null )
+                    refs.addAll( refs2 );
+                else
+                    refs = refs2;
+            }
+        }
+        return refs;
+    }
+
+    /**
+     * Find all pages that refer to this page. Returns null if the page
+     * does not exist or is not referenced at all, otherwise returns a
+     * collection containing page names (String) that refer to this one.
+     * <p>
+     * @param pagename The page to find referrers for.
+     * @return A Collection of Strings.  (This is, in fact, a Set, and is likely
+     *         to change at some point to a Set).  May return null, if the page
+     *         does not exist, or if it has no references.
+     */
+    // FIXME: Return a Set instead of a Collection.
+    public synchronized Collection<String> findReferrers( String pagename )
+    {
+        Set<String> refs = getReferenceList( m_referredBy, pagename );
+
+        if( refs == null || refs.isEmpty() )
+        {
+            return null;
+        }
+
+        return refs;
+
+    }
+
+    /**
+     *  Returns all pages that refer to this page.  Note that this method
+     *  returns an unmodifiable Map, which may be abruptly changed.  So any
+     *  access to any iterator may result in a ConcurrentModificationException.
+     *  <p>
+     *  The advantages of using this method over findReferrers() is that
+     *  it is very fast, as it does not create a new object.  The disadvantages
+     *  are that it does not do any mapping between plural names, and you
+     *  may end up getting a ConcurrentModificationException.
+     *
+     * @param pageName Page name to query.
+     * @return A Set of Strings containing the names of all the pages that refer
+     *         to this page.  May return null, if the page does not exist or
+     *         has not been indexed yet.
+     * @since 2.2.33
+     */
+    public Set findReferredBy( String pageName )
+    {
+        return m_unmutableReferredBy.get( getFinalPageName(pageName) );
+    }
+
+    /**
+     *  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 unmutable, 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 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.
+     * @since 2.2.33
+     */
+    public Collection<String> findRefersTo( String pageName )
+    {
+        return m_unmutableRefersTo.get( getFinalPageName(pageName) );
+    }
+
+    /**
+     * This 'deepHashCode' can be used to determine if there were any
+     * modifications made to the underlying to and by maps of the
+     * ReferenceManager. The maps of the ReferenceManager are not
+     * synchronized, so someone could add/remove entries in them while the
+     * hashCode is being computed.
+     *
+     * @return Sum of the hashCodes for the to and by maps of the
+     *         ReferenceManager
+     * @since 2.3.24
+     */
+    /*
+       This method traps and retries if a concurrent
+       modifcaition occurs.
+       TODO: It is unnecessary to calculate the hashcode; it should be calculated only
+             when the hashmaps are changed.  This is slow.
+    */
+    public int deepHashCode()
+    {
+        boolean failed = true;
+        int signature = 0;
+
+        while (failed)
+        {
+            signature = 0;
+            try
+            {
+                signature ^= m_referredBy.hashCode();
+                signature ^= m_refersTo.hashCode();
+                failed = false;
+            }
+            catch (ConcurrentModificationException e)
+            {
+                Thread.yield();
+            }
+        }
+
+        return signature;
+    }
+
+    /**
+     *  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.
+     *  @since 2.3.24
+     */
+    public Set<String> findCreated()
+    {
+        return new HashSet<String>( m_refersTo.keySet() );
+    }
+
+    private String getFinalPageName( String orig )
+    {
+        try
+        {
+            String s = m_engine.getFinalPageName( orig );
+
+            if( s == null ) s = orig;
+
+            return s;
+        }
+        catch( ProviderException e )
+        {
+            log.error("Error while trying to fetch a page name; trying to cope with the situation.",e);
+
+            return orig;
+        }
+    }
+
+    public void actionPerformed(WikiEvent event)
+    {
+        if( (event instanceof WikiPageEvent) && (event.getType() == WikiPageEvent.PAGE_DELETED) )
+        {
+            String pageName = ((WikiPageEvent) event).getPageName();
+
+            if( pageName != null )
+            {
+                pageRemoved( pageName );
+            }
+        }
+    }
+}

Added: incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/Release.java
URL: http://svn.apache.org/viewvc/incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/Release.java?rev=627255&view=auto
==============================================================================
--- incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/Release.java (added)
+++ incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/Release.java Tue Feb 12 21:53:55 2008
@@ -0,0 +1,193 @@
+/*
+    JSPWiki - a JSP-based WikiWiki clone.
+
+    Copyright (C) 2001-2007 Janne Jalkanen (Janne.Jalkanen@iki.fi)
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU Lesser General Public License as published by
+    the Free Software Foundation; either version 2.1 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Lesser General Public License for more details.
+
+    You should have received a copy of the GNU Lesser General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+package com.ecyrd.jspwiki;
+
+import org.apache.commons.lang.StringUtils;
+
+/**
+ *  Contains release and version information.  You may also invoke this
+ *  class directly, in which case it prints out the version string.  This
+ *  is a handy way of checking which JSPWiki version you have - just type
+ *  from a command line:
+ *  <pre>
+ *  % java -cp JSPWiki.jar com.ecyrd.jspwiki.Release
+ *  2.5.38
+ *  </pre>
+ *  <p>
+ *  As a historical curiosity, this is the oldest JSPWiki file.  According
+ *  to the CVS history, it dates from 6.7.2001, and it really hasn't changed
+ *  much since.
+ *  </p>
+ *  @author Janne Jalkanen
+ *  @since  1.0
+ */
+public final class Release
+{
+    private static final String VERSION_SEPARATORS = ".-";
+
+    /**
+     *  This is the default application name.
+     */
+    public static final String     APPNAME       = "JSPWiki";
+
+    /**
+     *  This should be empty when doing a release - otherwise
+     *  keep it as "cvs" so that whenever someone checks out the code,
+     *  they know it is a bleeding-edge version.  Other possible
+     *  values are "alpha" and "beta" for alpha and beta versions,
+     *  respectively.
+     *  <p>
+     *  If the POSTFIX is empty, it is not added to the version string.
+     */
+    private static final String    POSTFIX       = "rc";
+
+    /** The JSPWiki major version. */
+    public static final int        VERSION       = 2;
+
+    /** The JSPWiki revision. */
+    public static final int        REVISION      = 6;
+
+    /** The minor revision.  */
+    public static final int        MINORREVISION = 1;
+
+    /** The build number/identifier.  This is a String as opposed to an integer, just
+     *  so that people can add other identifiers to it.  The build number is incremented
+     *  every time a committer checks in code, and reset when the a release is made.
+     *  <p>
+     *  If you are a person who likes to build his own releases, we recommend that you
+     *  add your initials to this identifier (e.g. "13-jj", or 49-aj").
+     *  <p>
+     *  If the build identifier is empty, it is not added.
+     */
+    public static final String     BUILD         = "4";
+    
+    /**
+     *  This is the generic version string you should use
+     *  when printing out the version.  It is of the form "VERSION.REVISION.MINORREVISION[-POSTFIX][-BUILD]".
+     */
+    public static final String     VERSTR        =
+        VERSION+"."+REVISION+"."+MINORREVISION+ ((POSTFIX.length() != 0 ) ? "-"+POSTFIX : "") + ((BUILD.length() != 0 ? "-"+BUILD : ""));
+
+    /**
+     *  Private constructor prevents instantiation.
+     */
+    private Release()
+    {}
+
+    /**
+     *  This method is useful for templates, because hopefully it will
+     *  not be inlined, and thus any change to version number does not
+     *  need recompiling the pages.
+     *
+     *  @since 2.1.26.
+     *  @return The version string (e.g. 2.5.23).
+     */
+    public static String getVersionString()
+    {
+        return VERSTR;
+    }
+
+    /**
+     *  Returns true, if this version of JSPWiki is newer or equal than what is requested.
+     *  @param version A version parameter string (a.b.c-something). B and C are optional.
+     *  @return A boolean value describing whether the given version is newer than the current JSPWiki.
+     *  @since 2.4.57
+     *  @throws IllegalArgumentException If the version string could not be parsed.
+     */
+    public static boolean isNewerOrEqual( String version )
+        throws IllegalArgumentException
+    {
+        if( version == null ) return true;
+        String[] versionComponents = StringUtils.split(version,VERSION_SEPARATORS);
+        int reqVersion       = versionComponents.length > 0 ? Integer.parseInt(versionComponents[0]) : Release.VERSION;
+        int reqRevision      = versionComponents.length > 1 ? Integer.parseInt(versionComponents[1]) : Release.REVISION;
+        int reqMinorRevision = versionComponents.length > 2 ? Integer.parseInt(versionComponents[2]) : Release.MINORREVISION;
+
+        if( VERSION == reqVersion )
+        {
+            if( REVISION == reqRevision )
+            {
+                if( MINORREVISION == reqMinorRevision )
+                {
+                    return true;
+                }
+
+                return MINORREVISION > reqMinorRevision;
+            }
+
+            return REVISION > reqRevision;
+        }
+
+        return VERSION > reqVersion;
+    }
+
+    /**
+     *  Returns true, if this version of JSPWiki is older or equal than what is requested.
+     *  @param version A version parameter string (a.b.c-something)
+     *  @return A boolean value describing whether the given version is older than the current JSPWiki version
+     *  @since 2.4.57
+     *  @throws IllegalArgumentException If the version string could not be parsed.
+     */
+    public static boolean isOlderOrEqual( String version )
+        throws IllegalArgumentException
+    {
+        if( version == null ) return true;
+
+        String[] versionComponents = StringUtils.split(version,VERSION_SEPARATORS);
+        int reqVersion       = versionComponents.length > 0 ? Integer.parseInt(versionComponents[0]) : Release.VERSION;
+        int reqRevision      = versionComponents.length > 1 ? Integer.parseInt(versionComponents[1]) : Release.REVISION;
+        int reqMinorRevision = versionComponents.length > 2 ? Integer.parseInt(versionComponents[2]) : Release.MINORREVISION;
+
+        if( VERSION == reqVersion )
+        {
+            if( REVISION == reqRevision )
+            {
+                if( MINORREVISION == reqMinorRevision )
+                {
+                    return true;
+                }
+
+                return MINORREVISION < reqMinorRevision;
+            }
+
+            return REVISION < reqRevision;
+        }
+
+        return VERSION < reqVersion;
+    }
+
+    /**
+     *  Executing this class directly from command line prints out
+     *  the current version.  It is very useful for things like
+     *  different command line tools.
+     *  <P>Example:
+     *  <PRE>
+     *  % java com.ecyrd.jspwiki.Release
+     *  1.9.26-cvs
+     *  </PRE>
+     *
+     *  @param argv The argument string.  This class takes in no arguments.
+     */
+    public static void main( String[] argv )
+    {
+        System.out.println(VERSTR);
+    }
+}
\ No newline at end of file

Added: incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/SearchMatcher.java
URL: http://svn.apache.org/viewvc/incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/SearchMatcher.java?rev=627255&view=auto
==============================================================================
--- incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/SearchMatcher.java (added)
+++ incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/SearchMatcher.java Tue Feb 12 21:53:55 2008
@@ -0,0 +1,192 @@
+/*
+    JSPWiki - a JSP-based WikiWiki clone.
+
+    Copyright (C) 2001 Janne Jalkanen (Janne.Jalkanen@iki.fi)
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU Lesser General Public License as published by
+    the Free Software Foundation; either version 2.1 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Lesser General Public License for more details.
+
+    You should have received a copy of the GNU Lesser General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+package com.ecyrd.jspwiki;
+
+
+import java.io.IOException;
+import java.io.BufferedReader;
+import java.io.StringReader;
+
+/**
+ * SearchMatcher performs the task of matching a search query to a page's
+ * contents. This utility class is isolated to simplify WikiPageProvider
+ * implementations and to offer an easy target for upgrades. The upcoming(?)
+ * TranslatorReader rewrite will presumably invalidate this, among other things.
+ *
+ * @since 2.1.5
+ * @author ebu at ecyrd dot com
+ */
+// FIXME: Move to the "search" -package in 3.0
+public class SearchMatcher
+{
+    private QueryItem[] m_queries;
+    private WikiEngine m_engine;
+
+    /**
+     *  Creates a new SearchMatcher.
+     *  
+     *  @param engine The WikiEngine
+     *  @param queries A list of queries
+     */
+    public SearchMatcher( WikiEngine engine, QueryItem[] queries )
+    {
+        m_engine = engine;
+        m_queries = queries;
+    }
+
+    /**
+     * Compares the page content, available through the given stream, to the
+     * query items of this matcher. Returns a search result object describing
+     * the quality of the match.
+     *
+     * <p>This method would benefit of regexps (1.4) and streaming. FIXME!
+     * 
+     * @param wikiname The name of the page
+     * @param pageText The content of the page
+     * @return A SearchResult item, or null, there are no queries
+     * @throws IOException If reading page content fails
+     */
+    public SearchResult matchPageContent( String wikiname, String pageText )
+        throws IOException
+    {
+        if( m_queries == null )
+        {
+            return null;
+        }
+
+        int[] scores = new int[ m_queries.length ];
+        BufferedReader in = new BufferedReader( new StringReader( pageText ) );
+        String line = null;
+
+        while( (line = in.readLine()) != null )
+        {
+            line = line.toLowerCase();
+
+            for( int j = 0; j < m_queries.length; j++ )
+            {
+                int index = -1;
+
+                while( (index = line.indexOf( m_queries[j].word, index+1 )) != -1 )
+                {
+                    if( m_queries[j].type != QueryItem.FORBIDDEN )
+                    {
+                        scores[j]++; // Mark, found this word n times
+                    }
+                    else
+                    {
+                        // Found something that was forbidden.
+                        return null;
+                    }
+                }
+            }
+        }
+
+        //
+        //  Check that we have all required words.
+        //
+
+        int totalscore = 0;
+
+        for( int j = 0; j < scores.length; j++ )
+        {
+            // Give five points for each occurrence
+            // of the word in the wiki name.
+
+            if( wikiname.toLowerCase().indexOf( m_queries[j].word ) != -1 &&
+                m_queries[j].type != QueryItem.FORBIDDEN )
+                scores[j] += 5;
+
+            //  Filter out pages if the search word is marked 'required'
+            //  but they have no score.
+
+            if( m_queries[j].type == QueryItem.REQUIRED && scores[j] == 0 )
+            {
+                return null;
+            }
+
+            //
+            //  Count the total score for this page.
+            //
+            totalscore += scores[j];
+        }
+
+        if( totalscore > 0 )
+        {
+            return new SearchResultImpl( wikiname, totalscore );
+        }
+
+        return null;
+    }
+
+    /**
+     *  A local search result.
+     *  
+     */
+    public class SearchResultImpl
+        implements SearchResult
+    {
+        int      m_score;
+        WikiPage m_page;
+
+        /**
+         *  Create a new SearchResult with a given name and a score.
+         *  
+         *  @param name Page Name
+         *  @param score A score from 0+
+         */
+        public SearchResultImpl( String name, int score )
+        {
+            m_page  = new WikiPage( m_engine, name );
+            m_score = score;
+        }
+
+        /**
+         *  Returns Wikipage for this result.
+         *  @return WikiPage
+         */
+        public WikiPage getPage()
+        {
+            return m_page;
+        }
+
+        /**
+         *  Returns a score for this match.
+         *  
+         *  @return Score from 0+
+         */
+        public int getScore()
+        {
+            return m_score;
+        }
+
+        /**
+         *  Returns an empty array, since BasicSearchProvider does not support
+         *  context matching.
+         *  
+         *  @return an empty array
+         */
+        public String[] getContexts()
+        {
+            // Unimplemented
+            return new String[0];
+        }
+    }
+
+}

Added: incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/SearchResult.java
URL: http://svn.apache.org/viewvc/incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/SearchResult.java?rev=627255&view=auto
==============================================================================
--- incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/SearchResult.java (added)
+++ incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/SearchResult.java Tue Feb 12 21:53:55 2008
@@ -0,0 +1,54 @@
+/* 
+    JSPWiki - a JSP-based WikiWiki clone.
+
+    Copyright (C) 2001 Janne Jalkanen (Janne.Jalkanen@iki.fi)
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU Lesser General Public License as published by
+    the Free Software Foundation; either version 2.1 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Lesser General Public License for more details.
+
+    You should have received a copy of the GNU Lesser General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+package com.ecyrd.jspwiki;
+
+/**
+ *  Defines a search result.
+ *  
+ *  @author Janne Jalkanen
+ */
+// FIXME3.0: Move to the search-package
+public interface SearchResult
+{
+    /**
+     *  Return the page.
+     *  
+     *  @return the WikiPage object containing this result
+     */
+    public WikiPage getPage();
+
+    /**
+     *  Returns the score.
+     *  
+     *  @return A positive score value.  Note that there is no upper limit for the score.
+     */
+
+    public int getScore();
+    
+    
+    /**
+     * Collection of XHTML fragments representing some contexts in which
+     * the match was made (a.k.a., "snippets").
+     *
+     * @return the search results
+     * @since 2.4
+     */
+    public String[] getContexts();
+}

Added: incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/SearchResultComparator.java
URL: http://svn.apache.org/viewvc/incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/SearchResultComparator.java?rev=627255&view=auto
==============================================================================
--- incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/SearchResultComparator.java (added)
+++ incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/SearchResultComparator.java Tue Feb 12 21:53:55 2008
@@ -0,0 +1,52 @@
+/* 
+    JSPWiki - a JSP-based WikiWiki clone.
+
+    Copyright (C) 2001 Janne Jalkanen (Janne.Jalkanen@iki.fi)
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU Lesser General Public License as published by
+    the Free Software Foundation; either version 2.1 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Lesser General Public License for more details.
+
+    You should have received a copy of the GNU Lesser General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+package com.ecyrd.jspwiki;
+
+import java.io.Serializable;
+import java.util.*;
+
+/**
+ *  Simple class that decides which search results are more
+ *  important than others.
+ */
+// FIXME3.0: move to the search package
+public class SearchResultComparator
+    implements Comparator<SearchResult>, Serializable
+{
+    private static final long serialVersionUID = 1L;
+
+    /**
+     *  Compares two SearchResult objects, returning
+     *  the one that scored higher.
+     *  
+     *  {@inheritDoc}
+     */
+    public int compare( SearchResult s1, SearchResult s2 )
+    {
+        // Bigger scores are first.
+
+        int res = s2.getScore() - s1.getScore();
+
+        if( res == 0 )
+            res = s1.getPage().getName().compareTo(s2.getPage().getName());
+
+        return res;
+    }
+}

Added: incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/StringTransmutator.java
URL: http://svn.apache.org/viewvc/incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/StringTransmutator.java?rev=627255&view=auto
==============================================================================
--- incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/StringTransmutator.java (added)
+++ incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/StringTransmutator.java Tue Feb 12 21:53:55 2008
@@ -0,0 +1,37 @@
+/* 
+    JSPWiki - a JSP-based WikiWiki clone.
+
+    Copyright (C) 2001-2002 Janne Jalkanen (Janne.Jalkanen@iki.fi)
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU Lesser General Public License as published by
+    the Free Software Foundation; either version 2.1 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Lesser General Public License for more details.
+
+    You should have received a copy of the GNU Lesser General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+package com.ecyrd.jspwiki;
+
+/**
+ *  Defines an interface for transforming strings within a Wiki context.
+ *
+ *  @since 1.6.4
+ */
+public interface StringTransmutator
+{
+    /**
+     *  Returns a changed String, suitable for Wiki context.
+     *  
+     *  @param context WikiContext in which mutation is to be done
+     *  @param source  The source string.
+     *  @return The mutated string.
+     */
+    public String mutate( WikiContext context, String source );
+}

Added: incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/TextUtil.java
URL: http://svn.apache.org/viewvc/incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/TextUtil.java?rev=627255&view=auto
==============================================================================
--- incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/TextUtil.java (added)
+++ incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/TextUtil.java Tue Feb 12 21:53:55 2008
@@ -0,0 +1,831 @@
+/*
+    JSPWiki - a JSP-based WikiWiki clone.
+
+    Copyright (C) 2001-2002 Janne Jalkanen (Janne.Jalkanen@iki.fi)
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU Lesser General Public License as published by
+    the Free Software Foundation; either version 2.1 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Lesser General Public License for more details.
+
+    You should have received a copy of the GNU Lesser General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+package com.ecyrd.jspwiki;
+
+import java.io.UnsupportedEncodingException;
+import java.security.SecureRandom;
+import java.util.Properties;
+import java.util.Random;
+
+
+/**
+ *  Contains a number of static utility methods.
+ */
+// FIXME3.0: Move to the "util" package
+public final class TextUtil
+{
+    static final String   HEX_DIGITS = "0123456789ABCDEF";
+
+    /**
+     *  Private constructor prevents instantiation.
+     */
+    private TextUtil()
+    {}
+    
+    /**
+     *  java.net.URLEncoder.encode() method in JDK < 1.4 is buggy.  This duplicates
+     *  its functionality.
+     *  @param rs the string to encode
+     *  @return the URL-encoded string
+     */
+    protected static String urlEncode( byte[] rs )
+    {
+        StringBuffer result = new StringBuffer(rs.length*2);
+
+        // Does the URLEncoding.  We could use the java.net one, but
+        // it does not eat byte[]s.
+
+        for( int i = 0; i < rs.length; i++ )
+        {
+            char c = (char) rs[i];
+
+            switch( c )
+            {
+              case '_':
+              case '.':
+              case '*':
+              case '-':
+              case '/':
+                result.append( c );
+                break;
+
+              case ' ':
+                result.append( '+' );
+                break;
+
+              default:
+                if( (c >= 'a' && c <= 'z') ||
+                    (c >= 'A' && c <= 'Z') ||
+                    (c >= '0' && c <= '9') )
+                {
+                    result.append( c );
+                }
+                else
+                {
+                    result.append( '%' );
+                    result.append( HEX_DIGITS.charAt( (c & 0xF0) >> 4 ) );
+                    result.append( HEX_DIGITS.charAt( c & 0x0F ) );
+                }
+            }
+
+        } // for
+
+        return result.toString();
+    }
+
+    /**
+     *  URL encoder does not handle all characters correctly.
+     *  See <A HREF="http://developer.java.sun.com/developer/bugParade/bugs/4257115.html">
+     *  Bug parade, bug #4257115</A> for more information.
+     *  <P>
+     *  Thanks to CJB for this fix.
+     *  
+     *  @param bytes The byte array containing the bytes of the string
+     *  @param encoding The encoding in which the string should be interpreted
+     *  @return A decoded String
+     *  
+     *  @throws UnsupportedEncodingException If the encoding is unknown.
+     *  @throws IllegalArgumentException If the byte array is not a valid string.
+     */
+    protected static String urlDecode( byte[] bytes, String encoding )
+        throws UnsupportedEncodingException,
+               IllegalArgumentException
+    {
+        if(bytes == null)
+        {
+            return null;
+        }
+
+        byte[] decodeBytes   = new byte[bytes.length];
+        int decodedByteCount = 0;
+
+        try
+        {
+            for( int count = 0; count < bytes.length; count++ )
+            {
+                switch( bytes[count] )
+                {
+                  case '+':
+                    decodeBytes[decodedByteCount++] = (byte) ' ';
+                    break ;
+
+                  case '%':
+                    decodeBytes[decodedByteCount++] = (byte)((HEX_DIGITS.indexOf(bytes[++count]) << 4) +
+                                                             (HEX_DIGITS.indexOf(bytes[++count])) );
+
+                    break ;
+
+                  default:
+                    decodeBytes[decodedByteCount++] = bytes[count] ;
+                }
+            }
+
+        }
+        catch (IndexOutOfBoundsException ae)
+        {
+            throw new IllegalArgumentException( "Malformed UTF-8 string?" );
+        }
+
+        String processedPageName = null ;
+
+        try
+        {
+            processedPageName = new String(decodeBytes, 0, decodedByteCount, encoding) ;
+        }
+        catch (UnsupportedEncodingException e)
+        {
+            throw new UnsupportedEncodingException( "UTF-8 encoding not supported on this platform" );
+        }
+
+        return processedPageName;
+    }
+
+    /**
+     *  As java.net.URLEncoder class, but this does it in UTF8 character set.
+     *  
+     *  @param text The text to decode
+     *  @return An URLEncoded string.
+     */
+    public static String urlEncodeUTF8( String text )
+    {
+        // If text is null, just return an empty string
+        if ( text == null )
+        {
+            return "";
+        }
+
+        byte[] rs;
+
+        try
+        {
+            rs = text.getBytes("UTF-8");
+            return urlEncode( rs );
+        }
+        catch( UnsupportedEncodingException e )
+        {
+            throw new InternalWikiException("UTF-8 not supported!?!");
+        }
+
+    }
+
+    /**
+     *  As java.net.URLDecoder class, but for UTF-8 strings.  null is a safe
+     *  value and returns null.
+     *  
+     *  @param utf8 The UTF-8 encoded string
+     *  @return A plain, normal string.
+     */
+    public static String urlDecodeUTF8( String utf8 )
+    {
+        String rs = null;
+
+        if( utf8 == null ) return null;
+
+        try
+        {
+            rs = urlDecode( utf8.getBytes("ISO-8859-1"), "UTF-8" );
+        }
+        catch( UnsupportedEncodingException e )
+        {
+            throw new InternalWikiException("UTF-8 or ISO-8859-1 not supported!?!");
+        }
+
+        return rs;
+    }
+
+    /**
+     * Provides encoded version of string depending on encoding.
+     * Encoding may be UTF-8 or ISO-8859-1 (default).
+     *
+     * <p>This implementation is the same as in
+     * FileSystemProvider.mangleName().
+     * 
+     * @param data A string to encode
+     * @param encoding The encoding in which to encode
+     * @return An URL encoded string.
+     */
+    public static String urlEncode( String data, String encoding )
+    {
+        // Presumably, the same caveats apply as in FileSystemProvider.
+        // Don't see why it would be horribly kludgy, though.
+        if( "UTF-8".equals( encoding ) )
+        {
+            return TextUtil.urlEncodeUTF8( data );
+        }
+
+        try
+        {
+            return TextUtil.urlEncode( data.getBytes(encoding) );
+        }
+        catch (UnsupportedEncodingException uee)
+        {
+            throw new InternalWikiException("Could not encode String into" + encoding);
+        }
+    }
+
+    /**
+     * Provides decoded version of string depending on encoding.
+     * Encoding may be UTF-8 or ISO-8859-1 (default).
+     *
+     * <p>This implementation is the same as in
+     * FileSystemProvider.unmangleName().
+     * 
+     * @param data The URL-encoded string to decode
+     * @param encoding The encoding to use
+     * @return A decoded string.
+     * @throws UnsupportedEncodingException If the encoding is unknown
+     * @throws IllegalArgumentException If the data cannot be decoded.
+     */
+    public static String urlDecode( String data, String encoding )
+        throws UnsupportedEncodingException,
+               IllegalArgumentException
+    {
+        // Presumably, the same caveats apply as in FileSystemProvider.
+        // Don't see why it would be horribly kludgy, though.
+        if( "UTF-8".equals( encoding ) )
+        {
+            return TextUtil.urlDecodeUTF8( data );
+        }
+
+        try
+        {
+            return TextUtil.urlDecode( data.getBytes(encoding), encoding );
+        }
+        catch (UnsupportedEncodingException uee)
+        {
+            throw new InternalWikiException("Could not decode String into" + encoding);
+        }
+
+    }
+
+    /**
+     *  Replaces the relevant entities inside the String.
+     *  All &amp; &gt;, &lt;, and &quot; are replaced by their
+     *  respective names.
+     *
+     *  @since 1.6.1
+     *  @param src The source string.
+     *  @return The encoded string.
+     */
+    public static String replaceEntities( String src )
+    {
+        src = replaceString( src, "&", "&amp;" );
+        src = replaceString( src, "<", "&lt;" );
+        src = replaceString( src, ">", "&gt;" );
+        src = replaceString( src, "\"", "&quot;" );
+
+        return src;
+    }
+
+    /**
+     *  Replaces a string with an other string.
+     *
+     *  @param orig Original string.  Null is safe.
+     *  @param src  The string to find.
+     *  @param dest The string to replace <I>src</I> with.
+     *  @return A string with the replacement done.
+     */
+    public static final String replaceString( String orig, String src, String dest )
+    {
+        if ( orig == null ) return null;
+        if ( src == null || dest == null ) throw new NullPointerException();
+        if ( src.length() == 0 ) return orig;
+
+        StringBuffer res = new StringBuffer(orig.length()+20); // Pure guesswork
+        int start = 0;
+        int end = 0;
+        int last = 0;
+
+        while ( (start = orig.indexOf(src,end)) != -1 )
+        {
+            res.append( orig.substring( last, start ) );
+            res.append( dest );
+            end  = start+src.length();
+            last = start+src.length();
+        }
+
+        res.append( orig.substring( end ) );
+
+        return res.toString();
+    }
+
+    /**
+     *  Replaces a part of a string with a new String.
+     *
+     *  @param start Where in the original string the replacing should start.
+     *  @param end Where the replacing should end.
+     *  @param orig Original string.  Null is safe.
+     *  @param text The new text to insert into the string.
+     *  @return The string with the orig replaced with text.
+     */
+    public static String replaceString( String orig, int start, int end, String text )
+    {
+        if( orig == null ) return null;
+
+        StringBuffer buf = new StringBuffer(orig);
+
+        buf.replace( start, end, text );
+
+        return buf.toString();
+    }
+
+    /**
+     *  Parses an integer parameter, returning a default value
+     *  if the value is null or a non-number.
+     *  
+     *  @param value The value to parse
+     *  @param defvalue A default value in case the value is not a number
+     *  @return The parsed value (or defvalue).
+     */
+
+    public static int parseIntParameter( String value, int defvalue )
+    {
+        int val = defvalue;
+
+        try
+        {
+            val = Integer.parseInt( value.trim() );
+        }
+        catch( Exception e ) {}
+
+        return val;
+    }
+
+    /**
+     *  Gets an integer-valued property from a standard Properties
+     *  list.  If the value does not exist, or is a non-integer, returns defVal.
+     *
+     *  @since 2.1.48.
+     *  @param props The property set to look through
+     *  @param key   The key to look for
+     *  @param defVal If the property is not found or is a non-integer, returns this value.
+     *  @return The property value as an integer (or defVal).
+     */
+    public static int getIntegerProperty( Properties props,
+                                          String key,
+                                          int defVal )
+    {
+        String val = props.getProperty( key );
+
+        return parseIntParameter( val, defVal );
+    }
+
+    /**
+     *  Gets a boolean property from a standard Properties list.
+     *  Returns the default value, in case the key has not been set.
+     *  <P>
+     *  The possible values for the property are "true"/"false", "yes"/"no", or
+     *  "on"/"off".  Any value not recognized is always defined as "false".
+     *
+     *  @param props   A list of properties to search.
+     *  @param key     The property key.
+     *  @param defval  The default value to return.
+     *
+     *  @return True, if the property "key" was set to "true", "on", or "yes".
+     *
+     *  @since 2.0.11
+     */
+    public static boolean getBooleanProperty( Properties props,
+                                              String key,
+                                              boolean defval )
+    {
+        String val = props.getProperty( key );
+
+        if( val == null ) return defval;
+
+        return isPositive( val );
+    }
+
+    /**
+     *  Fetches a String property from the set of Properties.  This differs from
+     *  Properties.getProperty() in a couple of key respects: First, property value
+     *  is trim()med (so no extra whitespace back and front), and well, that's it.
+     *
+     *  @param props The Properties to search through
+     *  @param key   The property key
+     *  @param defval A default value to return, if the property does not exist.
+     *  @return The property value.
+     *  @since 2.1.151
+     */
+    public static String getStringProperty( Properties props,
+                                            String key,
+                                            String defval )
+    {
+        String val = props.getProperty( key );
+
+        if( val == null ) return defval;
+
+        return val.trim();
+    }
+
+    /**
+     *  Returns true, if the string "val" denotes a positive string.  Allowed
+     *  values are "yes", "on", and "true".  Comparison is case-insignificant.
+     *  Null values are safe.
+     *
+     *  @param val Value to check.
+     *  @return True, if val is "true", "on", or "yes"; otherwise false.
+     *
+     *  @since 2.0.26
+     */
+    public static boolean isPositive( String val )
+    {
+        if( val == null ) return false;
+
+        val = val.trim();
+
+        return val.equalsIgnoreCase("true") || val.equalsIgnoreCase("on") ||
+                 val.equalsIgnoreCase("yes");
+    }
+
+    /**
+     *  Makes sure that the POSTed data is conforms to certain rules.  These
+     *  rules are:
+     *  <UL>
+     *  <LI>The data always ends with a newline (some browsers, such
+     *      as NS4.x series, does not send a newline at the end, which makes
+     *      the diffs a bit strange sometimes.
+     *  <LI>The CR/LF/CRLF mess is normalized to plain CRLF.
+     *  </UL>
+     *
+     *  The reason why we're using CRLF is that most browser already
+     *  return CRLF since that is the closest thing to a HTTP standard.
+     *  
+     *  @param postData The data to normalize
+     *  @return Normalized data
+     */
+    public static String normalizePostData( String postData )
+    {
+        StringBuffer sb = new StringBuffer();
+
+        for( int i = 0; i < postData.length(); i++ )
+        {
+            switch( postData.charAt(i) )
+            {
+              case 0x0a: // LF, UNIX
+                sb.append( "\r\n" );
+                break;
+
+              case 0x0d: // CR, either Mac or MSDOS
+                sb.append( "\r\n" );
+                // If it's MSDOS, skip the LF so that we don't add it again.
+                if( i < postData.length()-1 && postData.charAt(i+1) == 0x0a )
+                {
+                    i++;
+                }
+                break;
+
+              default:
+                sb.append( postData.charAt(i) );
+                break;
+            }
+        }
+
+        if( sb.length() < 2 || !sb.substring( sb.length()-2 ).equals("\r\n") )
+        {
+            sb.append( "\r\n" );
+        }
+
+        return sb.toString();
+    }
+
+    private static final int EOI   = 0;
+    private static final int LOWER = 1;
+    private static final int UPPER = 2;
+    private static final int DIGIT = 3;
+    private static final int OTHER = 4;
+    private static final Random RANDOM = new SecureRandom();
+
+    private static int getCharKind(int c)
+    {
+        if (c==-1)
+        {
+            return EOI;
+        }
+
+        char ch = (char) c;
+
+        if (Character.isLowerCase(ch))
+            return LOWER;
+        else if (Character.isUpperCase(ch))
+            return UPPER;
+        else if (Character.isDigit(ch))
+            return DIGIT;
+        else
+            return OTHER;
+    }
+
+    /**
+     *  Adds spaces in suitable locations of the input string.  This is
+     *  used to transform a WikiName into a more readable format.
+     *
+     *  @param s String to be beautified.
+     *  @return A beautified string.
+     */
+    public static String beautifyString( String s )
+    {
+        return beautifyString( s, " " );
+    }
+
+    /**
+     *  Adds spaces in suitable locations of the input string.  This is
+     *  used to transform a WikiName into a more readable format.
+     *
+     *  @param s String to be beautified.
+     *  @param space Use this string for the space character.
+     *  @return A beautified string.
+     *  @since 2.1.127
+     */
+    public static String beautifyString( String s, String space )
+    {
+        StringBuffer result = new StringBuffer();
+
+        if( s == null || s.length() == 0 ) return "";
+
+        int cur     = s.charAt(0);
+        int curKind = getCharKind(cur);
+
+        int prevKind = LOWER;
+        int nextKind = -1;
+
+        int next = -1;
+        int nextPos = 1;
+
+        while( curKind != EOI )
+        {
+            next = (nextPos < s.length()) ? s.charAt(nextPos++) : -1;
+            nextKind = getCharKind( next );
+
+            if( (prevKind == UPPER) && (curKind == UPPER) && (nextKind == LOWER) )
+            {
+                result.append(space);
+                result.append((char) cur);
+            }
+            else
+            {
+                result.append((char) cur);
+                if( ( (curKind == UPPER) && (nextKind == DIGIT) )
+                    || ( (curKind == LOWER) && ((nextKind == DIGIT) || (nextKind == UPPER)) )
+                    || ( (curKind == DIGIT) && ((nextKind == UPPER) || (nextKind == LOWER)) ))
+                {
+                    result.append(space);
+                }
+            }
+            prevKind = curKind;
+            cur      = next;
+            curKind  = nextKind;
+        }
+
+        return result.toString();
+    }
+
+    /**
+     *  Creates a Properties object based on an array which contains alternatively
+     *  a key and a value.  It is useful for generating default mappings.
+     *  For example:
+     *  <pre>
+     *     String[] properties = { "jspwiki.property1", "value1",
+     *                             "jspwiki.property2", "value2 };
+     *
+     *     Properties props = TextUtil.createPropertes( values );
+     *
+     *     System.out.println( props.getProperty("jspwiki.property1") );
+     *  </pre>
+     *  would output "value1".
+     *
+     *  @param values Alternating key and value pairs.
+     *  @return Property object
+     *  @see java.util.Properties
+     *  @throws IllegalArgumentException if the property array is missing
+     *          a value for a key.
+     *  @since 2.2.
+     */
+
+    public static Properties createProperties( String[] values )
+        throws IllegalArgumentException
+    {
+        if( values.length % 2 != 0 )
+            throw new IllegalArgumentException( "One value is missing.");
+
+        Properties props = new Properties();
+
+        for( int i = 0; i < values.length; i += 2 )
+        {
+            props.setProperty( values[i], values[i+1] );
+        }
+
+        return props;
+    }
+
+    /**
+     *  Counts the number of sections (separated with "----") from the page.
+     *
+     *  @param pagedata The WikiText to parse.
+     *  @return int Number of counted sections.
+     *  @since 2.1.86.
+     */
+
+    public static int countSections( String pagedata )
+    {
+        int tags  = 0;
+        int start = 0;
+
+        while( (start = pagedata.indexOf("----",start)) != -1 )
+        {
+            tags++;
+            start+=4; // Skip this "----"
+        }
+
+        //
+        // The first section does not get the "----"
+        //
+        return pagedata.length() > 0 ? tags+1 : 0;
+    }
+
+    /**
+     *  Gets the given section (separated with "----") from the page text.
+     *  Note that the first section is always #1.  If a page has no section markers,
+     *  them there is only a single section, #1.
+     *
+     *  @param pagedata WikiText to parse.
+     *  @param section  Which section to get.
+     *  @return String  The section.
+     *  @throws IllegalArgumentException If the page does not contain this many sections.
+     *  @since 2.1.86.
+     */
+    public static String getSection( String pagedata, int section )
+        throws IllegalArgumentException
+    {
+        int tags  = 0;
+        int start = 0;
+        int previous = 0;
+
+        while( (start = pagedata.indexOf("----",start)) != -1 )
+        {
+            if( ++tags == section )
+            {
+                return pagedata.substring( previous, start );
+            }
+
+            start += 4; // Skip this "----"
+
+            previous = start;
+        }
+
+        if( ++tags == section )
+        {
+            return pagedata.substring( previous );
+        }
+
+        throw new IllegalArgumentException("There is no section no. "+section+" on the page.");
+    }
+
+    /**
+     *  A simple routine which just repeates the arguments.  This is useful
+     *  for creating something like a line or something.
+     *
+     *  @param what String to repeat
+     *  @param times How many times to repeat the string.
+     *  @return Guess what?
+     *  @since 2.1.98.
+     */
+    public static String repeatString( String what, int times )
+    {
+        StringBuffer sb = new StringBuffer();
+
+        for( int i = 0; i < times; i++ )
+        {
+            sb.append( what );
+        }
+
+        return sb.toString();
+    }
+
+    /**
+     *  Converts a string from the Unicode representation into something that can be
+     *  embedded in a java properties file.  All references outside the ASCII range
+     *  are replaced with \\uXXXX.
+     *
+     *  @param s The string to convert
+     *  @return the ASCII string
+     */
+    public static String native2Ascii(String s)
+    {
+        StringBuffer sb = new StringBuffer();
+        for(int i = 0; i < s.length(); i++)
+        {
+            char aChar = s.charAt(i);
+            if ((aChar < 0x0020) || (aChar > 0x007e))
+            {
+                sb.append('\\');
+                sb.append('u');
+                sb.append(toHex((aChar >> 12) & 0xF));
+                sb.append(toHex((aChar >>  8) & 0xF));
+                sb.append(toHex((aChar >>  4) & 0xF));
+                sb.append(toHex( aChar        & 0xF));
+            }
+            else
+            {
+                sb.append(aChar);
+            }
+        }
+        return sb.toString();
+    }
+
+    private static char toHex(int nibble)
+    {
+        final char[] hexDigit =
+        {
+            '0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F'
+        };
+        return hexDigit[nibble & 0xF];
+    }
+
+    /**
+     *  Generates a hexadecimal string from an array of bytes.  For
+     *  example, if the array contains { 0x01, 0x02, 0x3E }, the resulting
+     *  string will be "01023E".
+     *
+     * @param bytes A Byte array
+     * @return A String representation
+     * @since 2.3.87
+     */
+    public static String toHexString( byte[] bytes )
+    {
+        StringBuffer sb = new StringBuffer( bytes.length*2 );
+        for( int i = 0; i < bytes.length; i++ )
+        {
+            sb.append( toHex(bytes[i] >> 4) );
+            sb.append( toHex(bytes[i]) );
+        }
+
+        return sb.toString();
+    }
+
+    /**
+     *  Returns true, if the argument contains a number, otherwise false.
+     *  In a quick test this is roughly the same speed as Integer.parseInt()
+     *  if the argument is a number, and roughly ten times the speed, if
+     *  the argument is NOT a number.
+     *
+     *  @since 2.4
+     *  @param s String to check
+     *  @return True, if s represents a number.  False otherwise.
+     */
+
+    public static boolean isNumber( String s )
+    {
+        if( s == null ) return false;
+
+        if( s.length() > 1 && s.charAt(0) == '-' )
+            s = s.substring(1);
+
+        for( int i = 0; i < s.length(); i++ )
+        {
+            if( !Character.isDigit(s.charAt(i)) )
+                return false;
+        }
+
+        return true;
+    }
+
+    /** Length of password. @see #generateRandomPassword() */
+    public static final int PASSWORD_LENGTH = 8;
+    /**
+     * Generate a random String suitable for use as a temporary password.
+     *
+     * @return String suitable for use as a temporary password
+     * @since 2.4
+     */
+    public static String generateRandomPassword()
+    {
+        // Pick from some letters that won't be easily mistaken for each
+        // other. So, for example, omit o O and 0, 1 l and L.
+        String letters = "abcdefghjkmnpqrstuvwxyzABCDEFGHJKMNPQRSTUVWXYZ23456789+@";
+
+        String pw = "";
+        for (int i=0; i<PASSWORD_LENGTH; i++)
+        {
+            int index = (int)(RANDOM.nextDouble()*letters.length());
+            pw += letters.substring(index, index+1);
+        }
+        return pw;
+    }
+}