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 [19/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/diff/ContextualDiffProvider.java
URL: http://svn.apache.org/viewvc/incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/diff/ContextualDiffProvider.java?rev=627255&view=auto
==============================================================================
--- incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/diff/ContextualDiffProvider.java (added)
+++ incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/diff/ContextualDiffProvider.java Tue Feb 12 21:53:55 2008
@@ -0,0 +1,464 @@
+/*
+   JSPWiki - a JSP-based WikiWiki clone.
+
+   Copyright (C) 2001-2005 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.diff;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Properties;
+import java.util.StringTokenizer;
+
+import org.apache.commons.jrcs.diff.*;
+import org.apache.commons.jrcs.diff.myers.MyersDiff;
+import org.apache.log4j.Logger;
+
+import com.ecyrd.jspwiki.NoRequiredPropertyException;
+import com.ecyrd.jspwiki.TextUtil;
+import com.ecyrd.jspwiki.WikiContext;
+import com.ecyrd.jspwiki.WikiEngine;
+
+/**
+ * A seriously better diff provider, which highlights changes word-by-word using
+ * CSS.
+ *
+ * Suggested by John Volkar.
+ *
+ * @author John Volkar
+ * @author Janne Jalkanen
+ * @author <a href="mailto:hps@intermeta.de">Henning P. Schmiedehausen</a>
+ */
+
+public class ContextualDiffProvider implements DiffProvider
+{
+
+    private static final Logger log = Logger.getLogger( ContextualDiffProvider.class );
+
+    public static final String PROP_UNCHANGED_CONTEXT_LIMIT = "jspwiki.contextualDiffProvider.unchangedContextLimit";
+
+    //TODO all of these publics can become jspwiki.properties entries...
+    //TODO span title= can be used to get hover info...
+
+    public boolean m_emitChangeNextPreviousHyperlinks = true;
+
+    //Don't use spans here the deletion and insertions are nested in this...
+    public String m_changeStartHtml    = ""; //This could be a image '>' for a start marker
+    public String m_changeEndHtml      = ""; //and an image for an end '<' marker
+    public String m_diffStart          = "<div class=\"diff-wikitext\">";
+    public String m_diffEnd            = "</div>";
+
+    // Unfortunately we need to do dumb HTML here for RSS feeds.
+
+    public String m_insertionStartHtml = "<font color=\"#8000FF\"><span class=\"diff-insertion\">";
+    public String m_insertionEndHtml   = "</span></font>";
+    public String m_deletionStartHtml  = "<strike><font color=\"red\"><span class=\"diff-deletion\">";
+    public String m_deletionEndHtml    = "</span></font></strike>";
+    private String m_anchorPreIndex    = "<a name=\"change-";
+    private String m_anchorPostIndex   = "\" />";
+    private String m_backPreIndex      = "<a class=\"diff-nextprev\" title=\"Go to previous change\" href=\"#change-";
+    private String m_backPostIndex     = "\">&lt;&lt;</a>";
+    private String m_forwardPreIndex   = "<a class=\"diff-nextprev\" title=\"Go to next change\" href=\"#change-";
+    private String m_forwardPostIndex  = "\">&gt;&gt;</a>";
+    public String m_elidedHeadIndicatorHtml = "<br/><br/><b>...</b>";
+    public String m_elidedTailIndicatorHtml = "<b>...</b><br/><br/>";
+    public String m_lineBreakHtml = "<br />";
+    public String m_alternatingSpaceHtml = "&nbsp;";
+
+    // This one, I will make property file based...
+    private static final int LIMIT_MAX_VALUE = (Integer.MAX_VALUE /2) - 1;
+    private int m_unchangedContextLimit = LIMIT_MAX_VALUE;
+
+
+
+    public ContextualDiffProvider()
+    {}
+
+    /**
+     * @see com.ecyrd.jspwiki.WikiProvider#getProviderInfo()
+     */
+    public String getProviderInfo()
+    {
+        return "ContextualDiffProvider";
+    }
+
+    /**
+     * @see com.ecyrd.jspwiki.WikiProvider#initialize(com.ecyrd.jspwiki.WikiEngine,
+     *      java.util.Properties)
+     */
+    public void initialize(WikiEngine engine, Properties properties) throws NoRequiredPropertyException, IOException
+    {
+        String configuredLimit = properties.getProperty(PROP_UNCHANGED_CONTEXT_LIMIT, Integer
+            .toString(LIMIT_MAX_VALUE));
+        int limit = LIMIT_MAX_VALUE;
+        try
+        {
+            limit = Integer.parseInt(configuredLimit);
+        }
+        catch (NumberFormatException e)
+        {
+            log.warn("Failed to parseInt " + PROP_UNCHANGED_CONTEXT_LIMIT + "=" + configuredLimit
+                + "   Will use a huge number as limit.", e);
+        }
+        m_unchangedContextLimit = limit;
+    }
+
+
+
+    /**
+     * Do a colored diff of the two regions. This. is. serious. fun. ;-)
+     *
+     * @see com.ecyrd.jspwiki.diff.DiffProvider#makeDiffHtml(java.lang.String,
+     *      java.lang.String)
+     */
+    public synchronized String makeDiffHtml( WikiContext ctx, String wikiOld, String wikiNew )
+    {
+        //
+        // Sequencing handles lineterminator to <br /> and every-other consequtive space to a &nbsp;
+        //
+        String[] alpha = sequence( TextUtil.replaceEntities( wikiOld ) );
+        String[] beta  = sequence( TextUtil.replaceEntities( wikiNew ) );
+
+        Revision rev = null;
+        try
+        {
+            rev = Diff.diff( alpha, beta, new MyersDiff() );
+        }
+        catch( DifferentiationFailedException dfe )
+        {
+            log.error( "Diff generation failed", dfe );
+            return "Error while creating version diff.";
+        }
+
+        int revSize = rev.size();
+
+        StringBuffer sb = new StringBuffer();
+
+        sb.append( m_diffStart );
+
+        //
+        // The MyersDiff is a bit dumb by converting a single line multi-word diff into a series
+        // of Changes. The ChangeMerger pulls them together again...
+        //
+        ChangeMerger cm = new ChangeMerger( sb, alpha, revSize );
+
+        rev.accept( cm );
+
+        cm.shutdown();
+
+        sb.append( m_diffEnd );
+
+        return sb.toString();
+    }
+
+    /**
+     * Take the string and create an array from it, split it first on newlines, making
+     * sure to preserve the newlines in the elements, split each resulting element on
+     * spaces, preserving the spaces.
+     *
+     * All this preseving of newlines and spaces is so the wikitext when diffed will have fidelity
+     * to it's original form.  As a side affect we see edits of purely whilespace.
+     */
+    private String[] sequence( String wikiText )
+    {
+        String[] linesArray = Diff.stringToArray( wikiText );
+
+        List list = new ArrayList();
+
+        for( int i = 0; i < linesArray.length; i++ )
+        {
+            String line = linesArray[i];
+
+            String lastToken = null;
+            String token = null;
+            // StringTokenizer might be discouraged but it still is perfect here...
+            for (StringTokenizer st = new StringTokenizer( line, " ", true ); st.hasMoreTokens();)
+            {
+                token = st.nextToken();
+
+                if(" ".equals( lastToken) && " ".equals( token ))
+                    token = m_alternatingSpaceHtml;
+
+                list.add(token);
+                lastToken = token;
+            }
+
+            list.add(m_lineBreakHtml); // Line Break
+        }
+
+        return (String[])list.toArray( new String[0] );
+    }
+
+    /**
+     * This helper class does the housekeeping for merging
+     * our various changes down and also makes sure that the
+     * whole change process is threadsafe by encapsulating
+     * all necessary variables.
+     */
+    private final class ChangeMerger implements RevisionVisitor
+    {
+        private StringBuffer m_sb = null;
+
+        /** Keeping score of the original lines to process */
+        private int m_max = -1;
+
+        private int m_index = 0;
+
+        /** Index of the next element to be copied into the output. */
+        private int m_firstElem = 0;
+
+        /** Link Anchor counter */
+        private int m_count = 1;
+
+        /** State Machine Mode */
+        private int m_mode = -1; /* -1: Unset, 0: Add, 1: Del, 2: Change mode */
+
+        /** Buffer to coalesce the changes together */
+        private StringBuffer m_origBuf = null;
+
+        private StringBuffer m_newBuf = null;
+
+        /** Reference to the source string array */
+        private String[] m_origStrings = null;
+
+        private ChangeMerger( final StringBuffer sb, final String[] origStrings, final int max )
+        {
+            m_sb = sb;
+            m_origStrings = origStrings;
+            m_max = max;
+
+            m_origBuf = new StringBuffer();
+            m_newBuf = new StringBuffer();
+        }
+
+        private void updateState( Delta delta )
+        {
+            m_index++;
+
+            Chunk orig = delta.getOriginal();
+
+            if (orig.first() > m_firstElem)
+            {
+                // We "skip" some lines in the output.
+                // So flush out the last Change, if one exists.
+                flushChanges();
+
+                // Allow us to "skip" large swaths of unchanged text, show a "limited" amound of
+                // unchanged context so the changes are shown in
+                if ((orig.first() - m_firstElem) > 2 * m_unchangedContextLimit)
+                {
+                    if (m_firstElem > 0)
+                    {
+                        int endIndex = Math.min( m_firstElem + m_unchangedContextLimit, m_origStrings.length -1 );
+
+                        for (int j = m_firstElem; j < endIndex; j++)
+                            m_sb.append(m_origStrings[j]);
+
+                        m_sb.append(m_elidedTailIndicatorHtml);
+                    }
+
+                    m_sb.append(m_elidedHeadIndicatorHtml);
+
+                    int startIndex = Math.max(orig.first() - m_unchangedContextLimit, 0);
+                    for (int j = startIndex; j < orig.first(); j++)
+                        m_sb.append(m_origStrings[j]);
+
+                }
+                else
+                {
+                    // No need to skip anything, just output the whole range...
+                    for (int j = m_firstElem; j < orig.first(); j++)
+                    m_sb.append( m_origStrings[j] );
+                }
+            }
+            m_firstElem = orig.last() + 1;
+        }
+
+        public void visit( Revision rev )
+        {
+            // GNDN (Goes nowhere, does nothing)
+        }
+
+        public void visit( AddDelta delta )
+        {
+            updateState( delta );
+
+            // We have run Deletes up to now. Flush them out.
+            if( m_mode == 1 )
+            {
+                flushChanges();
+                m_mode = -1;
+            }
+            // We are in "neutral mode". Start a new Change
+            if( m_mode == -1 )
+            {
+                m_mode = 0;
+            }
+
+            // We are in "add mode".
+            if( m_mode == 0 || m_mode == 2 )
+            {
+                addNew( delta.getRevised() );
+                m_mode = 1;
+            }
+        }
+
+        public void visit( ChangeDelta delta )
+        {
+            updateState( delta );
+
+            // We are in "neutral mode". A Change might be merged with an add or delete.
+            if( m_mode == -1 )
+            {
+                m_mode = 2;
+            }
+
+            // Add the Changes to the buffers.
+            addOrig( delta.getOriginal() );
+            addNew( delta.getRevised() );
+        }
+
+        public void visit( DeleteDelta delta )
+        {
+            updateState( delta );
+
+            // We have run Adds up to now. Flush them out.
+            if( m_mode == 0 )
+            {
+                flushChanges();
+                m_mode = -1;
+            }
+            // We are in "neutral mode". Start a new Change
+            if( m_mode == -1 )
+            {
+                m_mode = 1;
+            }
+
+            // We are in "delete mode".
+            if( m_mode == 1 || m_mode == 2 )
+            {
+                addOrig( delta.getOriginal() );
+                m_mode = 1;
+            }
+        }
+
+        public void shutdown()
+        {
+            m_index = m_max + 1; // Make sure that no hyperlink gets created
+            flushChanges();
+
+            if (m_firstElem < m_origStrings.length)
+            {
+                // If there's more than the limit of the orginal left just emit limit and elided...
+                if ((m_origStrings.length - m_firstElem) > m_unchangedContextLimit)
+                {
+                    int endIndex = Math.min( m_firstElem + m_unchangedContextLimit, m_origStrings.length -1 );
+
+                    for (int j = m_firstElem; j < endIndex; j++)
+                    m_sb.append( m_origStrings[j] );
+
+                    m_sb.append(m_elidedTailIndicatorHtml);
+                }
+                else
+                // emit entire tail of original...
+                {
+                    for (int j = m_firstElem; j < m_origStrings.length; j++)
+                        m_sb.append(m_origStrings[j]);
+                }
+            }
+        }
+
+        private void addOrig( Chunk chunk )
+        {
+            if( chunk != null )
+            {
+                chunk.toString( m_origBuf );
+            }
+        }
+
+        private void addNew( Chunk chunk )
+        {
+            if( chunk != null )
+            {
+                chunk.toString( m_newBuf );
+            }
+        }
+
+
+
+        private void flushChanges()
+        {
+
+            if( m_newBuf.length() + m_origBuf.length() > 0 )
+            {
+                // This is the span element which encapsulates anchor and the change itself
+                m_sb.append( m_changeStartHtml );
+
+                // Do we want to have a "back link"?
+                if( m_emitChangeNextPreviousHyperlinks && m_count > 1 )
+                {
+                    m_sb.append( m_backPreIndex );
+                    m_sb.append( m_count - 1 );
+                    m_sb.append( m_backPostIndex );
+                }
+
+                // An anchor for the change.
+                if (m_emitChangeNextPreviousHyperlinks)
+                {
+                    m_sb.append( m_anchorPreIndex );
+                    m_sb.append( m_count++ );
+                    m_sb.append( m_anchorPostIndex );
+                }
+
+                // ... has been added
+                if( m_newBuf.length() > 0 )
+                {
+                    m_sb.append( m_insertionStartHtml );
+                    m_sb.append( m_newBuf );
+                    m_sb.append( m_insertionEndHtml );
+                }
+
+                // .. has been removed
+                if( m_origBuf.length() > 0 )
+                {
+                    m_sb.append( m_deletionStartHtml );
+                    m_sb.append( m_origBuf );
+                    m_sb.append( m_deletionEndHtml );
+                }
+
+                // Do we want a "forward" link?
+                if( m_emitChangeNextPreviousHyperlinks && (m_index < m_max) )
+                {
+                    m_sb.append( m_forwardPreIndex );
+                    m_sb.append( m_count ); // Has already been incremented.
+                    m_sb.append( m_forwardPostIndex );
+                }
+
+                m_sb.append( m_changeEndHtml );
+
+                // Nuke the buffers.
+                m_origBuf = new StringBuffer();
+                m_newBuf = new StringBuffer();
+            }
+
+            // After a flush, everything is reset.
+            m_mode = -1;
+        }
+    }
+}

Added: incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/diff/DiffProvider.java
URL: http://svn.apache.org/viewvc/incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/diff/DiffProvider.java?rev=627255&view=auto
==============================================================================
--- incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/diff/DiffProvider.java (added)
+++ incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/diff/DiffProvider.java Tue Feb 12 21:53:55 2008
@@ -0,0 +1,76 @@
+/* 
+    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.diff;
+
+import java.io.IOException;
+import java.util.Properties;
+
+import com.ecyrd.jspwiki.NoRequiredPropertyException;
+import com.ecyrd.jspwiki.WikiContext;
+import com.ecyrd.jspwiki.WikiEngine;
+import com.ecyrd.jspwiki.WikiProvider;
+
+/**
+ *  Provides an SPI for creating a diff between two page versions.
+ */
+public interface DiffProvider extends WikiProvider
+{
+    /**
+     * The return string is to be XHTML compliant ready to display html.  No further
+     * processing of this text will be done by the wiki engine.
+     * 
+     * @return An XHTML diff.
+     * @param context The Wiki Context
+     * @param oldWikiText the old text
+     * @param newWikiText the new text
+     */
+    public String makeDiffHtml(WikiContext context, String oldWikiText, String newWikiText);
+    
+    /**
+     *  If there is no diff provider set, this provider will work instead.
+     */
+    public static class NullDiffProvider implements DiffProvider
+    {
+        /**
+         *  {@inheritDoc}
+         */
+        public String makeDiffHtml(WikiContext ctx, String oldWikiText, String newWikiText)
+        {
+            return "You are using the NullDiffProvider, check your properties file.";
+        }
+
+        /**
+         *  {@inheritDoc}
+         */
+        public void initialize(WikiEngine engine, Properties properties) 
+            throws NoRequiredPropertyException, IOException
+        {
+        }
+
+        /**
+         *  {@inheritDoc}
+         */
+        public String getProviderInfo()
+        {
+            return "NullDiffProvider";
+        }
+    }
+    
+}

Added: incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/diff/DifferenceManager.java
URL: http://svn.apache.org/viewvc/incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/diff/DifferenceManager.java?rev=627255&view=auto
==============================================================================
--- incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/diff/DifferenceManager.java (added)
+++ incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/diff/DifferenceManager.java Tue Feb 12 21:53:55 2008
@@ -0,0 +1,139 @@
+/*
+   JSPWiki - a JSP-based WikiWiki clone.
+
+   Copyright (C) 2001-2005 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.diff;
+
+import java.io.IOException;
+import java.util.Properties;
+
+import org.apache.log4j.Logger;
+
+import com.ecyrd.jspwiki.NoRequiredPropertyException;
+import com.ecyrd.jspwiki.WikiContext;
+import com.ecyrd.jspwiki.WikiEngine;
+import com.ecyrd.jspwiki.util.ClassUtil;
+
+
+
+/**
+ * Load, initialize and delegate to the DiffProvider that will actually do the work.
+ *
+ * @author John Volkar
+ */
+public class DifferenceManager
+{
+    private static final Logger log = Logger.getLogger(DifferenceManager.class);
+
+    /** Property value for storing a diff provider.  Value is {@value}. */
+    public static final String PROP_DIFF_PROVIDER = "jspwiki.diffProvider";
+
+
+    private DiffProvider m_provider;
+
+    /**
+     *  Creates a new DifferenceManager for the given engine.
+     *  
+     *  @param engine The WikiEngine.
+     *  @param props A set of properties.
+     */
+    public DifferenceManager(WikiEngine engine, Properties props)
+    {
+        loadProvider(props);
+
+        initializeProvider(engine, props);
+
+        log.info("Using difference provider: " + m_provider.getProviderInfo());
+    }
+
+    private void loadProvider(Properties props)
+    {
+        String providerClassName = props.getProperty( PROP_DIFF_PROVIDER,
+                                                      TraditionalDiffProvider.class.getName() );
+
+        try
+        {
+            Class providerClass = ClassUtil.findClass( "com.ecyrd.jspwiki.diff", providerClassName );
+            m_provider = (DiffProvider)providerClass.newInstance();
+        }
+        catch( ClassNotFoundException e )
+        {
+            log.warn("Failed loading DiffProvider, will use NullDiffProvider.", e);
+        }
+        catch( InstantiationException e )
+        {
+            log.warn("Failed loading DiffProvider, will use NullDiffProvider.", e);
+        }
+        catch( IllegalAccessException e )
+        {
+            log.warn("Failed loading DiffProvider, will use NullDiffProvider.", e);
+        }
+
+        if( null == m_provider )
+        {
+            m_provider = new DiffProvider.NullDiffProvider();
+        }
+    }
+
+
+    private void initializeProvider(WikiEngine engine, Properties props)
+    {
+        try
+        {
+            m_provider.initialize( engine, props);
+        }
+        catch (NoRequiredPropertyException e1)
+        {
+            log.warn("Failed initializing DiffProvider, will use NullDiffProvider.", e1);
+            m_provider = new DiffProvider.NullDiffProvider(); //doesn't need init'd
+        }
+        catch (IOException e1)
+        {
+            log.warn("Failed initializing DiffProvider, will use NullDiffProvider.", e1);
+            m_provider = new DiffProvider.NullDiffProvider(); //doesn't need init'd
+        }
+    }
+
+    /**
+     *   Returns valid XHTML string to be used in any way you please.
+     *
+     *   @param context The Wiki Context
+     *   @param firstWikiText The old text
+     *   @param secondWikiText the new text
+     *   @return XHTML, or empty string, if no difference detected.
+     */
+    public String makeDiff(WikiContext context, String firstWikiText, String secondWikiText)
+    {
+        String diff = null;
+        try
+        {
+            diff = m_provider.makeDiffHtml( context, firstWikiText, secondWikiText);
+
+            if( diff == null )
+                diff = "";
+        }
+        catch(Exception e)
+        {
+            diff = "Failed to create a diff, check the logs.";
+            log.warn( diff, e);
+        }
+        return diff;
+    }
+}
+

Added: incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/diff/ExternalDiffProvider.java
URL: http://svn.apache.org/viewvc/incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/diff/ExternalDiffProvider.java?rev=627255&view=auto
==============================================================================
--- incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/diff/ExternalDiffProvider.java (added)
+++ incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/diff/ExternalDiffProvider.java Tue Feb 12 21:53:55 2008
@@ -0,0 +1,199 @@
+/*
+   JSPWiki - a JSP-based WikiWiki clone.
+
+   Copyright (C) 2001-2005 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.diff;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.IOException;
+import java.io.StringReader;
+import java.util.Properties;
+
+import org.apache.log4j.Logger;
+
+import com.ecyrd.jspwiki.*;
+
+/**
+ * This DiffProvider allows external command line tools to be used to generate
+ * the diff.
+ */
+public class ExternalDiffProvider implements DiffProvider
+{
+    private static final Logger log = Logger.getLogger(ExternalDiffProvider.class);
+
+    /**
+     * Determines the command to be used for 'diff'. This program must be able
+     * to output diffs in the unified format. For example 'diff -u %s1 %s2'.
+     */
+    public static final String PROP_DIFFCOMMAND    = "jspwiki.diffCommand";
+
+    private String m_diffCommand = null;
+    private String m_encoding;
+
+    private static final char DIFF_ADDED_SYMBOL    = '+';
+    private static final char DIFF_REMOVED_SYMBOL  = '-';
+
+    private static final String CSS_DIFF_ADDED     = "<tr><td bgcolor=\"#99FF99\" class=\"diffadd\">";
+    private static final String CSS_DIFF_REMOVED   = "<tr><td bgcolor=\"#FF9933\" class=\"diffrem\">";
+    private static final String CSS_DIFF_UNCHANGED = "<tr><td class=\"diff\">";
+    private static final String CSS_DIFF_CLOSE     = "</td></tr>";
+
+    //FIXME This could/should be a property for this provider, there is not guarentee that
+    //the external program generates a format suitible for the colorization code of the
+    //TraditionalDiffProvider, currently set to true for legacy compatibility.
+    //I don't think this 'feature' ever worked right, did it?...
+    private boolean m_traditionalColorization = true;
+
+    /**
+     *  Creates a new ExternalDiffProvider.
+     */
+    public ExternalDiffProvider()
+    {
+    }
+
+    /**
+     * @see com.ecyrd.jspwiki.WikiProvider#getProviderInfo()
+     * {@inheritDoc}
+     */
+    public String getProviderInfo()
+    {
+        return "ExternalDiffProvider";
+    }
+
+    /**
+     * {@inheritDoc}
+     * @see com.ecyrd.jspwiki.WikiProvider#initialize(com.ecyrd.jspwiki.WikiEngine, java.util.Properties)
+     */
+    public void initialize( WikiEngine engine, Properties properties )
+        throws NoRequiredPropertyException, IOException
+    {
+        m_diffCommand = properties.getProperty( PROP_DIFFCOMMAND );
+
+        if( (null == m_diffCommand) || (m_diffCommand.trim().equals( "" )) )
+        {
+            throw new NoRequiredPropertyException( "ExternalDiffProvider missing required property", PROP_DIFFCOMMAND );
+        }
+
+        m_encoding = engine.getContentEncoding();
+    }
+
+
+    /**
+     * Makes the diff by calling "diff" program.
+     * {@inheritDoc}
+     */
+    public String makeDiffHtml( WikiContext ctx, String p1, String p2 )
+    {
+        File f1 = null;
+        File f2 = null;
+        String diff = null;
+
+        try
+        {
+            f1 = FileUtil.newTmpFile(p1, m_encoding);
+            f2 = FileUtil.newTmpFile(p2, m_encoding);
+
+            String cmd = TextUtil.replaceString(m_diffCommand, "%s1", f1.getPath());
+            cmd = TextUtil.replaceString(cmd, "%s2", f2.getPath());
+
+            String output = FileUtil.runSimpleCommand(cmd, f1.getParent());
+
+            // FIXME: Should this rely on the system default encoding?
+            String rawWikiDiff = new String(output.getBytes("ISO-8859-1"), m_encoding);
+
+            String htmlWikiDiff = TextUtil.replaceEntities( rawWikiDiff );
+
+            if (m_traditionalColorization) //FIXME, see comment near declaration...
+                diff = colorizeDiff(diff);
+            else
+                diff = htmlWikiDiff;
+
+        }
+        catch (IOException e)
+        {
+            log.error("Failed to do file diff", e);
+        }
+        catch (InterruptedException e)
+        {
+            log.error("Interrupted", e);
+        }
+        finally
+        {
+            if( f1 != null )
+                f1.delete();
+            if( f2 != null )
+                f2.delete();
+        }
+
+        return diff;
+    }
+
+
+    /**
+     * Goes through output provided by a diff command and inserts HTML tags to
+     * make the result more legible. Currently colors lines starting with a +
+     * green, those starting with - reddish (hm, got to think of color blindness
+     * here...).
+     */
+    static String colorizeDiff(String diffText) throws IOException
+    {
+        if( diffText == null )
+            return "Invalid diff - probably something wrong with server setup.";
+
+        String line = null;
+        String start = null;
+        String stop = null;
+
+        BufferedReader in = new BufferedReader( new StringReader( diffText ) );
+        StringBuffer out = new StringBuffer();
+
+        out.append("<table class=\"diff\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\">\n");
+        while( (line = in.readLine()) != null )
+        {
+            stop = CSS_DIFF_CLOSE;
+
+            if( line.length() > 0 )
+            {
+                switch( line.charAt( 0 ) )
+                {
+                    case DIFF_ADDED_SYMBOL:
+                        start = CSS_DIFF_ADDED;
+                        break;
+                    case DIFF_REMOVED_SYMBOL:
+                        start = CSS_DIFF_REMOVED;
+                        break;
+                    default:
+                        start = CSS_DIFF_UNCHANGED;
+                }
+            }
+            else
+            {
+                start = CSS_DIFF_UNCHANGED;
+            }
+
+            out.append( start );
+            out.append( line.trim() );
+            out.append( stop + "\n" );
+        }
+
+        out.append( "</table>\n" );
+        return out.toString();
+    }
+}

Added: incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/diff/TraditionalDiffProvider.java
URL: http://svn.apache.org/viewvc/incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/diff/TraditionalDiffProvider.java?rev=627255&view=auto
==============================================================================
--- incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/diff/TraditionalDiffProvider.java (added)
+++ incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/diff/TraditionalDiffProvider.java Tue Feb 12 21:53:55 2008
@@ -0,0 +1,185 @@
+/*
+   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.diff;
+
+import java.io.IOException;
+import java.text.ChoiceFormat;
+import java.text.Format;
+import java.text.MessageFormat;
+import java.text.NumberFormat;
+import java.util.Properties;
+import java.util.ResourceBundle;
+
+import org.apache.commons.jrcs.diff.*;
+import org.apache.commons.jrcs.diff.myers.MyersDiff;
+import org.apache.log4j.Logger;
+
+import com.ecyrd.jspwiki.NoRequiredPropertyException;
+import com.ecyrd.jspwiki.TextUtil;
+import com.ecyrd.jspwiki.WikiContext;
+import com.ecyrd.jspwiki.WikiEngine;
+import com.ecyrd.jspwiki.i18n.InternationalizationManager;
+
+
+/**
+ * This is the JSPWiki 'traditional' diff.
+ * @author Janne Jalkanen
+ * @author Erik Bunn
+ * @author <a href="mailto:hps@intermeta.de">Henning P. Schmiedehausen</a>
+ */
+
+public class TraditionalDiffProvider implements DiffProvider
+{
+    private static final Logger log = Logger.getLogger(TraditionalDiffProvider.class);
+
+    private static final String CSS_DIFF_ADDED = "<tr><td class=\"diffadd\">";
+    private static final String CSS_DIFF_REMOVED = "<tr><td class=\"diffrem\">";
+    private static final String CSS_DIFF_UNCHANGED = "<tr><td class=\"diff\">";
+    private static final String CSS_DIFF_CLOSE = "</td></tr>" + Diff.NL;
+
+
+    public TraditionalDiffProvider()
+    {
+    }
+
+
+    /**
+     * @see com.ecyrd.jspwiki.WikiProvider#getProviderInfo()
+     */
+    public String getProviderInfo()
+    {
+        return "TraditionalDiffProvider";
+    }
+
+    /**
+     * @see com.ecyrd.jspwiki.WikiProvider#initialize(com.ecyrd.jspwiki.WikiEngine, java.util.Properties)
+     */
+    public void initialize(WikiEngine engine, Properties properties)
+        throws NoRequiredPropertyException, IOException
+    {
+    }
+
+    /**
+     * Makes a diff using the BMSI utility package. We use our own diff printer,
+     * which makes things easier.
+     */
+    public String makeDiffHtml( WikiContext ctx, String p1, String p2 )
+    {
+        String diffResult = "";
+
+        try
+        {
+            String[] first  = Diff.stringToArray(TextUtil.replaceEntities(p1));
+            String[] second = Diff.stringToArray(TextUtil.replaceEntities(p2));
+            Revision rev = Diff.diff(first, second, new MyersDiff());
+
+            if( rev == null || rev.size() == 0 )
+            {
+                // No difference
+
+                return "";
+            }
+
+            StringBuffer ret = new StringBuffer(rev.size() * 20); // Guessing how big it will become...
+
+            ret.append("<table class=\"diff\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\">\n");
+            rev.accept( new RevisionPrint(ctx,ret) );
+            ret.append("</table>\n");
+
+            return ret.toString();
+        }
+        catch( DifferentiationFailedException e )
+        {
+            diffResult = "makeDiff failed with DifferentiationFailedException";
+            log.error(diffResult, e);
+        }
+
+        return diffResult;
+    }
+
+
+    public static final class RevisionPrint
+        implements RevisionVisitor
+    {
+        private StringBuffer m_result = null;
+        private WikiContext  m_context;
+        private ResourceBundle m_rb;
+        
+        private RevisionPrint(WikiContext ctx,StringBuffer sb)
+        {
+            m_result = sb;
+            m_context = ctx;
+            m_rb = ctx.getBundle( InternationalizationManager.CORE_BUNDLE );
+        }
+
+        public void visit(Revision rev)
+        {
+            // GNDN (Goes nowhere, does nothing)
+        }
+
+        public void visit(AddDelta delta)
+        {
+            Chunk changed = delta.getRevised();
+            print(changed, m_rb.getString( "diff.traditional.added" ) );
+            changed.toString(m_result, CSS_DIFF_ADDED, CSS_DIFF_CLOSE);
+        }
+
+        public void visit(ChangeDelta delta)
+        {
+            Chunk changed = delta.getOriginal();
+            print(changed, m_rb.getString( "diff.traditional.changed") );
+            changed.toString(m_result, CSS_DIFF_REMOVED, CSS_DIFF_CLOSE);
+            delta.getRevised().toString(m_result, CSS_DIFF_ADDED, CSS_DIFF_CLOSE);
+        }
+
+        public void visit(DeleteDelta delta)
+        {
+            Chunk changed = delta.getOriginal();
+            print(changed, m_rb.getString( "diff.traditional.removed") );
+            changed.toString(m_result, CSS_DIFF_REMOVED, CSS_DIFF_CLOSE);
+        }
+
+        private void print(Chunk changed, String type)
+        {
+            m_result.append(CSS_DIFF_UNCHANGED);
+            
+            String[] choiceString = 
+            {
+               m_rb.getString("diff.traditional.oneline"),
+               m_rb.getString("diff.traditional.lines")
+            };
+            double[] choiceLimits = { 1, 2 };
+            
+            MessageFormat fmt = new MessageFormat("");
+            fmt.setLocale( m_context.getContext().getLocale() );
+            ChoiceFormat cfmt = new ChoiceFormat( choiceLimits, choiceString );
+            fmt.applyPattern( type );
+            Format[] formats = { NumberFormat.getInstance(), cfmt, NumberFormat.getInstance() };
+            fmt.setFormats( formats );
+            
+            Object[] params = { new Integer(changed.first() + 1), 
+                                new Integer(changed.size()),
+                                new Integer(changed.size()) };
+            m_result.append( fmt.format(params) );
+            m_result.append(CSS_DIFF_CLOSE);
+        }
+    }
+}

Added: incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/event/PageEventFilter.java
URL: http://svn.apache.org/viewvc/incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/event/PageEventFilter.java?rev=627255&view=auto
==============================================================================
--- incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/event/PageEventFilter.java (added)
+++ incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/event/PageEventFilter.java Tue Feb 12 21:53:55 2008
@@ -0,0 +1,166 @@
+/*
+    JSPWiki - a JSP-based WikiWiki clone.
+
+    Copyright (C) 2001-2006 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.event;
+
+import com.ecyrd.jspwiki.filters.BasicPageFilter;
+import com.ecyrd.jspwiki.filters.FilterException;
+
+import com.ecyrd.jspwiki.WikiContext;
+import java.util.Properties;
+
+/**
+  * Fires WikiPageEvents for page events.
+  * <p>
+  * Adding a PageEventFilter to the FilterManager will automatically
+  * attach an event delegate with the WikiEventManager to provide for
+  * firing and listener management. All that remains is then adding
+  * the listener to the filter via the WikiEventManager. This is quite
+  * simple:
+  * </p>
+  * <pre>
+  *    PageEventFilter filter = new PageEventFilter();
+  *    engine.getFilterManager().addPageFilter(filter,5000);
+  *    // attach listener to filter
+  *    WikiEventManager.addWikiEventListener(filter,listener);
+  * </pre>
+  * <p>
+  * This class provides convenience methods for adding and removing
+  * WikiEventListeners.
+  * </p>
+  *
+  * @see com.ecyrd.jspwiki.event.WikiEventManager
+  * @author Murray Altheim
+  */
+public class PageEventFilter extends BasicPageFilter
+{
+
+    /**
+      * Called whenever a new PageFilter is instantiated and reset.
+      */
+    public void initialize( Properties properties )
+            throws FilterException
+    {
+        //
+    }
+
+    /**
+      * This method is called whenever a page has been loaded from the provider,
+      * but not yet been sent through the TranslatorReader.  Note that you cannot
+      * do HTML translation here, because TranslatorReader is likely to escape it.
+      *
+      * @param wikiContext The current wikicontext.
+      * @param content     WikiMarkup.
+      */
+    public String preTranslate( WikiContext wikiContext, String content )
+            throws FilterException
+    {
+        fireEvent( WikiPageEvent.PRE_TRANSLATE, wikiContext );
+        return content;
+    }
+
+
+    /**
+      * This method is called after a page has been fed through the TranslatorReader,
+      * so anything you are seeing here is translated content.  If you want to
+      * do any of your own WikiMarkup2HTML translation, do it here.
+      */
+    public String postTranslate( WikiContext wikiContext, String htmlContent )
+            throws FilterException
+    {
+        fireEvent( WikiPageEvent.POST_TRANSLATE, wikiContext );
+        return htmlContent;
+    }
+
+
+    /**
+      * This method is called before the page has been saved to the PageProvider.
+      */
+    public String preSave( WikiContext wikiContext, String content )
+            throws FilterException
+    {
+        fireEvent( WikiPageEvent.PRE_SAVE, wikiContext );
+        return content;
+    }
+
+
+    /**
+      * This method is called after the page has been successfully saved.
+      * If the saving fails for any reason, then this method will not
+      * be called.
+      * <p>
+      * Since the result is discarded from this method, this is only useful
+      * for things like counters, etc.
+      */
+    public void postSave( WikiContext wikiContext, String content )
+            throws FilterException
+    {
+        fireEvent( WikiPageEvent.POST_SAVE, wikiContext );
+    }
+
+
+    // events processing .......................................................
+
+
+    /**
+     *  Registers a WikiEventListener with this instance.
+     *  This is a convenience method.
+     *
+     * @param listener the event listener
+     */
+    public final synchronized void addWikiEventListener( WikiEventListener listener )
+    {
+        WikiEventManager.addWikiEventListener( this, listener );
+    }
+
+    /**
+     *  Un-registers a WikiEventListener with this instance.
+     *  This is a convenience method.
+     *
+     * @param listener the event listener
+     */
+    public final synchronized void removeWikiEventListener( WikiEventListener listener )
+    {
+        WikiEventManager.removeWikiEventListener( this, listener );
+    }
+
+    /**
+     *  Fires a WikiPageEvent of the provided type and page name
+     *  to all registered listeners. Only <tt>PAGE_LOCK</tt> and
+     *  <tt>PAGE_UNLOCK</tt> event types will fire an event; other
+     *  event types are ignored.
+     *
+     * @see com.ecyrd.jspwiki.event.WikiPageEvent
+     * @param type      the WikiPageEvent type to be fired.
+     * @param context   the WikiContext of the event.
+     */
+    protected final void fireEvent( int type, WikiContext context )
+    {
+        if ( WikiEventManager.isListening(this) && WikiPageEvent.isValidType(type) )
+        {
+            WikiPageEvent event = new WikiPageEvent(
+                    context.getEngine(),
+                    type,
+                    context.getPage().getName());
+            WikiEventManager.fireEvent(this,event);
+        }
+    }
+
+} // end com.ecyrd.jspwiki.event.PageEventFilter

Added: incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/event/WikiEngineEvent.java
URL: http://svn.apache.org/viewvc/incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/event/WikiEngineEvent.java?rev=627255&view=auto
==============================================================================
--- incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/event/WikiEngineEvent.java (added)
+++ incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/event/WikiEngineEvent.java Tue Feb 12 21:53:55 2008
@@ -0,0 +1,160 @@
+/*
+    JSPWiki - a JSP-based WikiWiki clone.
+
+    Copyright (C) 2001-2006 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.event;
+
+import com.ecyrd.jspwiki.WikiEngine;
+
+/**
+  * WikiEngineEvent indicates a change in the state of the WikiEngine.
+  * 
+  * @author  Murray Altheim
+  * @author  Andrew Jaquith
+  * @see     com.ecyrd.jspwiki.event.WikiEvent
+  * @since   2.4.20
+  */
+public class WikiEngineEvent extends WikiEvent
+{
+    private static final long serialVersionUID = 1829433967558773970L;
+
+    /** Indicates a WikiEngine initialization event, fired as the 
+      * wiki service is being initialized (in progress). */
+    public static final int INITIALIZING   = -1;
+
+    /** Indicates a WikiEngine initialized event, fired after the 
+      * wiki service is fully available. */
+    public static final int INITIALIZED    = 0;
+
+    /** Indicates a WikiEngine closing event, fired as a signal that
+      * the wiki service is shutting down. */
+    public static final int SHUTDOWN       = 1;
+
+    /** Indicates a WikiEngine stopped event, fired after halting the wiki service.
+      * A WikiEngine in this state is not expected to provide further services. 
+      */
+    public static final int STOPPED        = 2;
+
+    private WikiEngine m_engine;
+
+    // ............
+
+
+     /**
+      *  Constructs an instance of this event.
+      * @param eventSource  the Object that is the source of the event,
+      * which <b>must</b> be the WikiEngine. If it is not, this
+      * method thows a ClassCastException
+      * @param type the event type
+      */
+    public WikiEngineEvent( Object eventSource, int type )
+    {
+        super( eventSource, type );
+        m_engine = (WikiEngine)eventSource;
+    }
+
+
+    /**
+     *  Sets the type of this event.
+     *
+     * @param type      the type of this WikiEngineEvent.
+     */
+    protected void setType( int type )
+    {
+        if ( type >= INITIALIZING && type <= STOPPED )
+        {
+            super.setType(type);
+        }
+        else
+        {
+            super.setType(ERROR);
+        }
+    }
+
+
+    /**
+     *  Returns the WikiEngine that spawned this event.
+     *
+     * @return  the WikiEngine that spawned this event.
+     */
+    public WikiEngine getEngine()
+    {
+        return m_engine;
+    }
+
+
+    /**
+     *  Returns the WikiEngine that spawned this event.
+     *
+     * @return  the WikiEngine that spawned this event.
+     * @deprecated  use {@link #getEngine()} instead.
+     */
+    public WikiEngine getWikiEngine()
+    {
+        return m_engine;
+    }
+
+
+   /**
+     * Returns <code>true</code> if the int value is a WikiPageEvent type.
+     * @param type the event type
+     * @return the result
+     */
+    public static boolean isValidType( int type )
+    {
+        return type >= INITIALIZING && type <= STOPPED;
+    }
+
+
+    /**
+     *  Returns a textual representation of the event type.
+     *
+     * @return a String representation of the type
+     */
+    public final String eventName()
+    {
+        switch ( getType() )
+        {
+            case INITIALIZING:         return "INITIALIZING";
+            case INITIALIZED:          return "INITIALIZED";
+            case SHUTDOWN:             return "SHUTDOWN";
+            case STOPPED:              return "STOPPED";
+            default:                   return super.eventName();
+        }
+    }
+
+
+    /**
+     *  Returns a human-readable description of the event type.
+     *
+     * @return a String description of the type
+     */
+    public final String getTypeDescription()
+    {
+        switch ( getType() )
+        {
+            case INITIALIZING:         return "wiki engine initializing";
+            case INITIALIZED:          return "wiki engine initialized";
+            case SHUTDOWN:             return "wiki engine shutting down";
+            case STOPPED:              return "wiki engine stopped";
+            default:                   return super.getTypeDescription();
+        }
+    }
+
+} // end class com.ecyrd.jspwiki.event.WikiEngineEvent

Added: incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/event/WikiEvent.java
URL: http://svn.apache.org/viewvc/incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/event/WikiEvent.java?rev=627255&view=auto
==============================================================================
--- incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/event/WikiEvent.java (added)
+++ incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/event/WikiEvent.java Tue Feb 12 21:53:55 2008
@@ -0,0 +1,157 @@
+/*
+    JSPWiki - a JSP-based WikiWiki clone.
+
+    Copyright (C) 2001-2006 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.event;
+
+import java.util.EventObject;
+
+/**
+ * Abstract parent class for wiki events.
+ *
+ * @author  Murray Altheim
+ * @author  Andrew Jaquith
+ * @since 2.3.79
+ */
+public abstract class WikiEvent extends EventObject
+{
+    private static final long serialVersionUID = 1829433967558773960L;
+
+    /** Indicates a exception or error state. */
+    public static final int ERROR          = -99;
+
+    /** Indicates an undefined state. */
+    public static final int UNDEFINED      = -98;
+
+    private int m_type = UNDEFINED;
+
+    private final long m_when;
+
+    // ............
+
+
+   /**
+     * Constructs an instance of this event.
+     * @param src the Object that is the source of the event.
+     * @param type   the event type.
+     */
+    public WikiEvent( Object src, int type )
+    {
+        super( src );
+        m_when = System.currentTimeMillis();
+        setType( type );
+    }
+
+
+    /**
+     *  Returns the timestamp of when this WikiEvent occurred.
+     *
+     * @return this event's timestamp
+     * @since 2.4.74
+     */
+    public long getWhen()
+    {
+        return m_when;
+    }
+
+
+   /**
+     * Sets the type of this event. Validation of acceptable
+     * type values is the responsibility of each subclass.
+     *
+     * @param type      the type of this WikiEvent.
+     */
+    protected void setType( int type )
+    {
+        m_type = type;
+    }
+
+
+   /**
+     * Returns the type of this event.
+     *
+     * @return  the type of this WikiEvent. See the enumerated values
+     *          defined in {@link com.ecyrd.jspwiki.event.WikiEvent}).
+     */
+    public int getType()
+    {
+        return m_type;
+    }
+
+
+   /** Returns a String (human-readable) description of an event type.
+     * This should be subclassed as necessary.
+     * @return the String description
+     */
+    public String getTypeDescription()
+    {
+        switch ( m_type )
+        {
+            case ERROR:                return "exception or error event";
+            case UNDEFINED:            return "undefined event type";
+            default:                   return "unknown event type (" + m_type + ")";
+        }
+    }
+
+
+   /**
+     * Returns true if the int value is a valid WikiEvent type.
+     * Because the WikiEvent class does not itself any event types,
+     * this method returns true if the event type is anything except
+     * {@link #ERROR} or {@link #UNDEFINED}. This method is meant to
+     * be subclassed as appropriate.
+     */
+    public static boolean isValidType( int type )
+    {
+        return type != ERROR && type != UNDEFINED;
+    }
+
+
+    /**
+     * Returns a textual representation of an event type.
+     * @return the String representation
+     */
+    public String eventName()
+    {
+        switch( m_type )
+        {
+            case ERROR:                return "ERROR";
+            case UNDEFINED:            return "UNDEFINED";
+            default:                   return "UNKNOWN (" + m_type + ")";
+        }
+    }
+
+    /**
+     * Prints a String (human-readable) representation of this object.
+     * This should be subclassed as necessary.
+     * @see java.lang.Object#toString()
+     * @return the String representation
+     */
+    public String toString()
+    {
+        StringBuffer out = new StringBuffer();
+        out.append( "WikiEvent." );
+        out.append( eventName() );
+        out.append( " [source=" );
+        out.append( getSource().toString() );
+        out.append( "]" );
+        return out.toString();
+    }
+
+}

Added: incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/event/WikiEventListener.java
URL: http://svn.apache.org/viewvc/incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/event/WikiEventListener.java?rev=627255&view=auto
==============================================================================
--- incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/event/WikiEventListener.java (added)
+++ incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/event/WikiEventListener.java Tue Feb 12 21:53:55 2008
@@ -0,0 +1,42 @@
+/*
+    JSPWiki - a JSP-based WikiWiki clone.
+
+    Copyright (C) 2001-2006 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.event;
+
+import  java.util.EventListener;
+
+/**
+  * Defines an interface for an object that listens for WikiEvents.
+  *
+  * @author  Murray Altheim
+  * @since   2.3.92
+  */
+public interface WikiEventListener extends EventListener
+{
+
+   /**
+     * Fired when a WikiEvent is triggered by an event source.
+     *
+     * @param event    a WikiEvent object
+     */
+    public void actionPerformed( WikiEvent event );
+
+
+} // end com.ecryd.jspwiki.event.WikiEventListener

Added: incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/event/WikiEventManager.java
URL: http://svn.apache.org/viewvc/incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/event/WikiEventManager.java?rev=627255&view=auto
==============================================================================
--- incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/event/WikiEventManager.java (added)
+++ incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/event/WikiEventManager.java Tue Feb 12 21:53:55 2008
@@ -0,0 +1,618 @@
+/*
+    JSPWiki - a JSP-based WikiWiki clone.
+
+    Copyright (C) 2001-2006 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.event;
+
+import java.lang.ref.WeakReference;
+import java.util.*;
+
+import org.apache.log4j.Logger;
+
+/**
+ *  A singleton class that manages the addition and removal of WikiEvent
+ *  listeners to a event source, as well as the firing of events to those
+ *  listeners. An "event source" is the object delegating its event
+ *  handling to an inner delegating class supplied by this manager. The
+ *  class being serviced is considered a "client" of the delegate. The
+ *  WikiEventManager operates across any number of simultaneously-existing
+ *  WikiEngines since it manages all delegation on a per-object basis.
+ *  Anything that might fire a WikiEvent (or any of its subclasses) can be
+ *  a client.
+ *  </p>
+ *
+ *  <h3>Using a Delegate for Event Listener Management</h3>
+ *  <p>
+ *  Basically, rather than have all manner of client classes maintain their
+ *  own listener lists, add and remove listener methods, any class wanting
+ *  to attach listeners can simply request a delegate object to provide that
+ *  service. The delegate handles the listener list, the add and remove
+ *  listener methods. Firing events is then a matter of calling the
+ *  WikiEventManager's {@link #fireEvent(Object,WikiEvent)} method, where
+ *  the Object is the client. Prior to instantiating the event object, the
+ *  client can call {@link #isListening(Object)} to see there are any
+ *  listeners attached to its delegate.
+ *  </p>
+ *
+ *  <h3>Adding Listeners</h3>
+ *  <p>
+ *  Adding a WikiEventListener to an object is very simple:
+ *  </p>
+ *  <pre>
+ *      WikiEventManager.addWikiEventListener(object,listener);
+ *  </pre>
+ *
+ *  <h3>Removing Listeners</h3>
+ *  <p>
+ *  Removing a WikiEventListener from an object is very simple:
+ *  </p>
+ *  <pre>
+ *      WikiEventManager.removeWikiEventListener(object,listener);
+ *  </pre>
+ *  If you only have a reference to the listener, the following method
+ *  will remove it from any clients managed by the WikiEventManager:
+ *  <pre>
+ *      WikiEventManager.removeWikiEventListener(listener);
+ *  </pre>
+ *
+ *  <h3>Backward Compatibility: Replacing Existing <tt>fireEvent()</tt> Methods</h3>
+ *  <p>
+ *  Using one manager for all events processing permits consolidation of all event
+ *  listeners and their associated methods in one place rather than having them
+ *  attached to specific subcomponents of an application, and avoids a great deal
+ *  of event-related cut-and-paste code. Convenience methods that call the
+ *  WikiEventManager for event delegation can be written to maintain existing APIs.
+ *  </p>
+ *  <p>
+ *  For example, an existing <tt>fireEvent()</tt> method might look something like
+ *  this:
+ *  </p>
+ *  <pre>
+ *    protected final void fireEvent( WikiEvent event )
+ *    {
+ *        for ( Iterator it = m_listeners.iterator(); it.hasNext(); )
+ *        {
+ *            WikiEventListener listener = (WikiEventListener)it.next();
+ *            listener.actionPerformed(event);
+ *        }
+ *    }
+ *  </pre>
+ *  <p>
+ *  One disadvantage is that the above method is supplied with event objects,
+ *  which are created even when no listener exists for them. In a busy wiki
+ *  with many users unused/unnecessary event creation could be considerable.
+ *  Another advantage is that in addition to the iterator, there must be code
+ *  to support the addition and remove of listeners. The above could be
+ *  replaced with the below code (and with no necessary local support for
+ *  adding and removing listeners):
+ *  </p>
+ *  <pre>
+ *    protected final void fireEvent( int type )
+ *    {
+ *        if ( WikiEventManager.isListening(this) )
+ *        {
+ *            WikiEventManager.fireEvent(this,new WikiEngineEvent(this,type));
+ *        }
+ *    }
+ *  </pre>
+ *  <p>
+ *  This only needs to be customized to supply the specific parameters for
+ *  whatever WikiEvent you want to create.
+ *  </p>
+ *
+ *  <h3 id="preloading">Preloading Listeners</h3>
+ *  <p>
+ *  This may be used to create listeners for objects that don't yet exist,
+ *  particularly designed for embedded applications that need to be able
+ *  to listen for the instantiation of an Object, by maintaining a cache
+ *  of client-less WikiEvent sources that set their client upon being
+ *  popped from the cache. Each time any of the methods expecting a client
+ *  parameter is called with a null parameter it will preload an internal
+ *  cache with a client-less delegate object that will be popped and
+ *  returned in preference to creating a new object. This can have unwanted
+ *  side effects if there are multiple clients populating the cache with
+ *  listeners. The only check is for a Class match, so be aware if others
+ *  might be populating the client-less cache with listeners.
+ *  </p>
+ *  <h3>Listener lifecycle</h3>
+ *  <p>
+ *  Note that in most cases it is not necessary to remove a listener.
+ *  As of 2.4.97, the listeners are stored as WeakReferences, and will be
+ *  automatically cleaned at the next garbage collection, if you no longer
+ *  hold a reference to them.  Of course, until the garbage is collected,
+ *  your object might still be getting events, so if you wish to avoid that,
+ *  please remove it explicitly as described above.
+ *  </p>
+ * @author Murray Altheim
+ * @since 2.4.20
+ */
+public final class WikiEventManager
+{
+    private static final Logger log = Logger.getLogger(WikiEventManager.class);
+
+    /* If true, permits a WikiEventMonitor to be set. */
+    private static boolean c_permitMonitor = false;
+
+    /* Optional listener to be used as all-event monitor. */
+    private static WikiEventListener c_monitor = null;
+
+    /* The Map of client object to WikiEventDelegate. */
+    private final Map m_delegates = new HashMap();
+
+    /* The Vector containing any preloaded WikiEventDelegates. */
+    private final Vector m_preloadCache = new Vector();
+
+    /* Singleton instance of the WikiEventManager. */
+    private static WikiEventManager c_instance = null;
+
+    // ............
+
+    /**
+     *  Constructor for a WikiEventManager.
+     */
+    private WikiEventManager()
+    {
+        c_instance = this;
+        log.debug("instantiated WikiEventManager");
+    }
+
+    /**
+     *  As this is a singleton class, this returns the single
+     *  instance of this class provided with the property file
+     *  filename and bit-wise application settings.
+     *
+     *  @return A shared instance of the WikiEventManager
+     */
+    public static WikiEventManager getInstance()
+    {
+        if( c_instance == null )
+        {
+            synchronized( WikiEventManager.class )
+            {
+                WikiEventManager mgr = new WikiEventManager();
+                // start up any post-instantiation services here
+                return mgr;
+            }
+        }
+        return c_instance;
+    }
+
+
+    // public/API methods ......................................................
+
+
+    /**
+     *  Registers a WikiEventListener with a WikiEventDelegate for
+     *  the provided client object.
+     *
+     *  <h3>Monitor Listener</h3>
+     *
+     *  If <tt>client</tt> is a reference to the WikiEventManager class
+     *  itself and the compile-time flag {@link #c_permitMonitor} is true,
+     *  this attaches the listener as an all-event monitor, overwriting
+     *  any previous listener (hence returning true).
+     *  <p>
+     *  You can remove any existing monitor by either calling this method
+     *  with <tt>client</tt> as a reference to this class and the
+     *  <tt>listener</tt> parameter as null, or
+     *  {@link #removeWikiEventListener(Object,WikiEventListener)} with a
+     *  <tt>client</tt> as a reference to this class. The <tt>listener</tt>
+     *  parameter in this case is ignored.
+     *
+     * @param client   the client of the event source
+     * @param listener the event listener
+     * @return true if the listener was added (i.e., it was not already in the list and was added)
+     */
+    public static final boolean addWikiEventListener(
+            Object client, WikiEventListener listener )
+    {
+        if ( client == WikiEventManager.class )
+        {
+            if ( c_permitMonitor ) c_monitor = listener;
+            return c_permitMonitor;
+        }
+        WikiEventDelegate delegate = getInstance().getDelegateFor(client);
+        return delegate.addWikiEventListener(listener);
+    }
+
+
+    /**
+     *  Un-registers a WikiEventListener with the WikiEventDelegate for
+     *  the provided client object.
+     *
+     * @param client   the client of the event source
+     * @param listener the event listener
+     * @return true if the listener was found and removed.
+     */
+    public static final boolean removeWikiEventListener(
+            Object client, WikiEventListener listener )
+    {
+        if ( client == WikiEventManager.class )
+        {
+            c_monitor = null;
+            return true;
+        }
+        WikiEventDelegate delegate = getInstance().getDelegateFor(client);
+        return delegate.removeWikiEventListener(listener);
+    }
+
+
+    /**
+     *  Return the Set containing the WikiEventListeners attached to the
+     *  delegate for the supplied client. If there are no attached listeners,
+     *  returns an empty Iterator rather than null.  Note that this will
+     *  create a delegate for the client if it did not exist prior to the call.
+     *
+     *  <h3>NOTE</h3>
+     *  <p>
+     *  This method returns a Set rather than an Iterator because of the high
+     *  likelihood of the Set being modified while an Iterator might be active.
+     *  This returns an unmodifiable reference to the actual Set containing
+     *  the delegate's listeners. Any attempt to modify the Set will throw an
+     *  {@link java.lang.UnsupportedOperationException}. This method is not
+     *  synchronized and it should be understood that the composition of the
+     *  backing Set may change at any time.
+     *  </p>
+     *
+     * @param client   the client of the event source
+     * @return an unmodifiable Set containing the WikiEventListeners attached to the client
+     * @throws java.lang.UnsupportedOperationException  if any attempt is made to modify the Set
+     */
+    public static final Set getWikiEventListeners( Object client )
+        throws UnsupportedOperationException
+    {
+        WikiEventDelegate delegate = getInstance().getDelegateFor(client);
+        return delegate.getWikiEventListeners();
+    }
+
+
+    /**
+     *  Un-registers a WikiEventListener from any WikiEventDelegate client managed by this
+     *  WikiEventManager. Since this is a one-to-one relation, the first match will be
+     *  returned upon removal; a true return value indicates the WikiEventListener was
+     *  found and removed.
+     *
+     * @param listener the event listener
+     * @return true if the listener was found and removed.
+     */
+    public static final boolean removeWikiEventListener( WikiEventListener listener )
+    {
+        // get the Map.entry object for the entire Map, then check match on entry (listener)
+        WikiEventManager mgr = getInstance();
+        Map sources = mgr.getDelegates();
+        synchronized( sources )
+        {
+            // get an iterator over the Map.Enty objects in the map
+            Iterator it = sources.entrySet().iterator();
+            while( it.hasNext() )
+            {
+                Map.Entry entry = (Map.Entry)it.next();
+                // the entry value is the delegate
+                WikiEventDelegate delegate = (WikiEventDelegate)entry.getValue();
+
+                // now see if we can remove the listener from the delegate
+                // (delegate may be null because this is a weak reference)
+                if( delegate != null && delegate.removeWikiEventListener(listener) )
+                {
+                    return true; // was removed
+                }
+            }
+        }
+        return false;
+    }
+
+
+    /**
+     *  Returns true if there are one or more listeners registered with
+     *  the provided client Object (undelegated event source). This locates
+     *  any delegate and checks to see if it has any listeners attached.
+     *
+     *  @param client the client Object
+     *  @return True, if there is a listener for this client object.
+     */
+    public static boolean isListening( Object client )
+    {
+        WikiEventDelegate source = getInstance().getDelegateFor(client);
+        return source != null ? source.isListening() : false ;
+    }
+
+
+    /**
+     *  Notify all listeners of the WikiEventDelegate having a registered
+     *  interest in change events of the supplied WikiEvent.
+     *
+     * @param client the client initiating the event.
+     * @param event  the WikiEvent to fire.
+     */
+    public static void fireEvent( Object client, WikiEvent event )
+    {
+        WikiEventDelegate source = getInstance().getDelegateFor(client);
+        if ( source != null ) source.fireEvent(event);
+        if ( c_monitor != null ) c_monitor.actionPerformed(event);
+    }
+
+
+    // private and utility methods .............................................
+
+
+    /**
+     *  Return the client-to-delegate Map.
+     */
+    private Map getDelegates()
+    {
+        return m_delegates;
+    }
+
+
+    /**
+     *  Returns a WikiEventDelegate for the provided client Object.
+     *  If the parameter is a class reference, will generate and return a
+     *  client-less WikiEventDelegate. If the parameter is not a Class and
+     *  the delegate cache contains any objects matching the Class of any
+     *  delegates in the cache, the first Class-matching delegate will be
+     *  used in preference to creating a new delegate.
+     *  If a null parameter is supplied, this will create a client-less
+     *  delegate that will attach to the first incoming client (i.e.,
+     *  there will be no Class-matching requirement).
+     *
+     * @param client   the client Object, or alternately a Class reference
+     * @return the WikiEventDelegate.
+     */
+    private WikiEventDelegate getDelegateFor( Object client )
+    {
+        synchronized( m_delegates )
+        {
+            if( client == null || client instanceof Class ) // then preload the cache
+            {
+                WikiEventDelegate delegate = new WikiEventDelegate(client);
+                m_preloadCache.add(delegate);
+                m_delegates.put( client, delegate );
+                return delegate;
+            }
+            else if( !m_preloadCache.isEmpty() )
+            {
+                // then see if any of the cached delegates match the class of the incoming client
+                for( int i = m_preloadCache.size()-1 ; i >= 0 ; i-- ) // start with most-recently added
+                {
+                    WikiEventDelegate delegate = (WikiEventDelegate)m_preloadCache.elementAt(i);
+                    if( delegate.getClientClass() == null
+                        || delegate.getClientClass().equals(client.getClass()) )
+                    {
+                        // we have a hit, so use it, but only on a client we haven't seen before
+                        if( !m_delegates.keySet().contains(client) )
+                        {
+                            m_preloadCache.remove(delegate);
+                            m_delegates.put( client, delegate );
+                            return delegate;
+                        }
+                    }
+                }
+            }
+            // otherwise treat normally...
+            WikiEventDelegate delegate = (WikiEventDelegate)m_delegates.get( client );
+            if( delegate == null )
+            {
+                delegate = new WikiEventDelegate( client );
+                m_delegates.put( client, delegate );
+            }
+            return delegate;
+        }
+    }
+
+
+    // .........................................................................
+
+
+    /**
+     *  Inner delegating class that manages event listener addition and
+     *  removal. Classes that generate events can obtain an instance of
+     *  this class from the WikiEventManager and delegate responsibility
+     *  to it. Interaction with this delegating class is done via the
+     *  methods of the {@link WikiEventDelegate} API.
+     *
+     * @author Murray Altheim
+     * @since 2.4.20
+     */
+    private static final class WikiEventDelegate
+    {
+        /* A list of event listeners for this instance. */
+
+        private ArrayList m_listenerList = new ArrayList();
+
+        private Class  m_class  = null;
+
+        /**
+         *  Constructor for an WikiEventDelegateImpl, provided
+         *  with the client Object it will service, or the Class
+         *  of client, the latter when used to preload a future
+         *  incoming delegate.
+         */
+        protected WikiEventDelegate( Object client )
+        {
+            if( client instanceof Class )
+            {
+                m_class = (Class)client;
+            }
+        }
+
+        /**
+         *  Returns the class of the client-less delegate, null if
+         *  this delegate is attached to a client Object.
+         */
+        protected Class getClientClass()
+        {
+            return m_class;
+        }
+
+
+        /**
+         *  Return an unmodifiable Set containing the WikiEventListeners of
+         *  this WikiEventDelegateImpl. If there are no attached listeners,
+         *  returns an empty Set rather than null.
+         *
+         * @return an unmodifiable Set containing this delegate's WikiEventListeners
+         * @throws java.lang.UnsupportedOperationException  if any attempt is made to modify the Set
+         */
+        public Set getWikiEventListeners()
+        {
+            synchronized( m_listenerList )
+            {
+                TreeSet set = new TreeSet( new WikiEventListenerComparator() );
+
+                for( Iterator i = m_listenerList.iterator(); i.hasNext(); )
+                {
+                    WikiEventListener l = (WikiEventListener) ((WeakReference)i.next()).get();
+
+                    if( l != null )
+                    {
+                        set.add( l );
+                    }
+                }
+
+                return Collections.unmodifiableSet(set);
+            }
+        }
+
+
+        /**
+         *  Adds <tt>listener</tt> as a listener for events fired by the WikiEventDelegate.
+         *
+         * @param listener   the WikiEventListener to be added
+         * @return true if the listener was added (i.e., it was not already in the list and was added)
+         */
+        public boolean addWikiEventListener( WikiEventListener listener )
+        {
+            synchronized( m_listenerList )
+            {
+                return m_listenerList.add( new WeakReference(listener) );
+            }
+        }
+
+
+        /**
+         *  Removes <tt>listener</tt> from the WikiEventDelegate.
+         *
+         * @param listener   the WikiEventListener to be removed
+         * @return true if the listener was removed (i.e., it was actually in the list and was removed)
+         */
+        public boolean removeWikiEventListener( WikiEventListener listener )
+        {
+            synchronized( m_listenerList )
+            {
+                for( Iterator i = m_listenerList.iterator(); i.hasNext(); )
+                {
+                    WikiEventListener l = (WikiEventListener) ((WeakReference)i.next()).get();
+
+                    if( l == listener )
+                    {
+                        i.remove();
+                        return true;
+                    }
+                }
+            }
+
+            return false;
+        }
+
+
+        /**
+         *  Returns true if there are one or more listeners registered
+         *  with this instance.
+         */
+        public boolean isListening()
+        {
+            synchronized( m_listenerList )
+            {
+                return !m_listenerList.isEmpty();
+            }
+        }
+
+
+        /**
+         *  Notify all listeners having a registered interest
+         *  in change events of the supplied WikiEvent.
+         */
+        public void fireEvent( WikiEvent event )
+        {
+            boolean needsCleanup = false;
+
+            try
+            {
+                synchronized( m_listenerList )
+                {
+                    for( int i = 0; i < m_listenerList.size(); i++ )
+                    {
+                        WikiEventListener listener = (WikiEventListener) ((WeakReference)m_listenerList.get(i)).get();
+
+                        if( listener != null )
+                        {
+                            listener.actionPerformed( event );
+                        }
+                        else
+                        {
+                            needsCleanup  = true;
+                        }
+                    }
+
+                    //
+                    //  Remove all such listeners which have expired
+                    //
+                    if( needsCleanup )
+                    {
+                        for( int i = 0; i < m_listenerList.size(); i++ )
+                        {
+                            WeakReference w = (WeakReference)m_listenerList.get(i);
+
+                            if( w.get() == null ) m_listenerList.remove(i--);
+                        }
+                    }
+
+                }
+            }
+            catch( ConcurrentModificationException e )
+            {
+                //
+                //  We don't die, we just don't do notifications in that case.
+                //
+                log.info("Concurrent modification of event list; please report this.",e);
+            }
+
+        }
+
+    } // end inner class WikiEventDelegate
+
+    private static class WikiEventListenerComparator implements Comparator
+    {
+        // TODO: This method is a critical performance bottleneck
+        public int compare(Object arg0, Object arg1)
+        {
+            if( arg0 instanceof WikiEventListener && arg1 instanceof WikiEventListener )
+            {
+                WikiEventListener w0 = (WikiEventListener) arg0;
+                WikiEventListener w1 = (WikiEventListener) arg1;
+
+                if( w1 == w0 || w0.equals(w1) ) return 0;
+
+                return w1.hashCode() - w0.hashCode();
+            }
+
+            throw new ClassCastException( arg1.getClass().getName() + " != " + arg0.getClass().getName() );
+        }
+    }
+} // end com.ecyrd.jspwiki.event.WikiEventManager