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 [14/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/auth/authorize/GroupManager.java
URL: http://svn.apache.org/viewvc/incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/auth/authorize/GroupManager.java?rev=627255&view=auto
==============================================================================
--- incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/auth/authorize/GroupManager.java (added)
+++ incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/auth/authorize/GroupManager.java Tue Feb 12 21:53:55 2008
@@ -0,0 +1,744 @@
+/*
+ * JSPWiki - a JSP-based WikiWiki clone. Copyright (C) 2001-2003 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.auth.authorize;
+
+import java.security.Principal;
+import java.util.*;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.apache.commons.lang.ArrayUtils;
+import org.apache.log4j.Logger;
+
+import com.ecyrd.jspwiki.NoRequiredPropertyException;
+import com.ecyrd.jspwiki.WikiContext;
+import com.ecyrd.jspwiki.WikiEngine;
+import com.ecyrd.jspwiki.WikiException;
+import com.ecyrd.jspwiki.WikiSession;
+import com.ecyrd.jspwiki.auth.*;
+import com.ecyrd.jspwiki.auth.user.UserProfile;
+import com.ecyrd.jspwiki.event.WikiEvent;
+import com.ecyrd.jspwiki.event.WikiEventListener;
+import com.ecyrd.jspwiki.event.WikiEventManager;
+import com.ecyrd.jspwiki.event.WikiSecurityEvent;
+import com.ecyrd.jspwiki.ui.InputValidator;
+import com.ecyrd.jspwiki.util.ClassUtil;
+
+/**
+ * <p>
+ * Facade class for storing, retrieving and managing wiki groups on behalf of
+ * AuthorizationManager, JSPs and other presentation-layer classes. GroupManager
+ * works in collaboration with a back-end {@link GroupDatabase}, which persists
+ * groups to permanent storage.
+ * </p>
+ * <p>
+ * <em>Note: prior to JSPWiki 2.4.19, GroupManager was an interface; it
+ * is now a concrete, final class. The aspects of GroupManager which previously
+ * extracted group information from storage (e.g., wiki pages) have been
+ * refactored into the GroupDatabase interface.</em>
+ * </p>
+ * @author Andrew Jaquith
+ * @since 2.4.19
+ */
+public final class GroupManager implements Authorizer, WikiEventListener
+{
+    /** Key used for adding UI messages to a user's WikiSession. */
+    public static final String  MESSAGES_KEY       = "group";
+
+    private static final String PROP_GROUPDATABASE = "jspwiki.groupdatabase";
+
+    static final Logger         log                = Logger.getLogger( GroupManager.class );
+
+    protected WikiEngine        m_engine;
+
+    protected WikiEventListener m_groupListener;
+
+    private GroupDatabase       m_groupDatabase    = null;
+
+    /** Map with GroupPrincipals as keys, and Groups as values */
+    private final Map<Principal,Group> m_groups    = new HashMap<Principal,Group>();
+
+    /**
+     * <p>
+     * Returns a GroupPrincipal matching a given name. If a group cannot be
+     * found, return <code>null</code>.
+     * </p>
+     * @param name Name of the group. This is case-sensitive.
+     * @return A DefaultGroup instance.
+     */
+    public Principal findRole( String name )
+    {
+        try
+        {
+            Group group = getGroup( name );
+            return group.getPrincipal();
+        }
+        catch( NoSuchPrincipalException e )
+        {
+            return null;
+        }
+    }
+
+    /**
+     * Returns the Group matching a given name. If the group cannot be found,
+     * this method throws a <code>NoSuchPrincipalException</code>.
+     * @param name the name of the group to find
+     * @return the group
+     * @throws NoSuchPrincipalException if the group cannot be found
+     */
+    public final Group getGroup( String name ) throws NoSuchPrincipalException
+    {
+        Group group = m_groups.get( new GroupPrincipal( name ) );
+        if ( group != null )
+        {
+            return group;
+        }
+        throw new NoSuchPrincipalException( "Group " + name + " not found." );
+    }
+
+    /**
+     * Returns the current external {@link GroupDatabase} in use. This method
+     * is guaranteed to return a properly-initialized GroupDatabase, unless
+     * it could not be initialized. In that case, this method throws
+     * a {@link com.ecyrd.jspwiki.WikiException}. The GroupDatabase
+     * is lazily initialized.
+     * @throws com.ecyrd.jspwiki.auth.WikiSecurityException if the GroupDatabase could
+     * not be initialized
+     * @return the current GroupDatabase
+     * @since 2.3
+     */
+    public final GroupDatabase getGroupDatabase() throws WikiSecurityException
+    {
+        if ( m_groupDatabase != null )
+        {
+            return m_groupDatabase;
+        }
+
+        String dbClassName = "<unknown>";
+        String dbInstantiationError = null;
+        Throwable cause = null;
+        try
+        {
+            Properties props = m_engine.getWikiProperties();
+            dbClassName = props.getProperty( PROP_GROUPDATABASE );
+            if ( dbClassName == null )
+            {
+                dbClassName = XMLGroupDatabase.class.getName();
+            }
+            log.info( "Attempting to load group database class " + dbClassName );
+            Class dbClass = ClassUtil.findClass( "com.ecyrd.jspwiki.auth.authorize", dbClassName );
+            m_groupDatabase = (GroupDatabase) dbClass.newInstance();
+            m_groupDatabase.initialize( m_engine, m_engine.getWikiProperties() );
+            log.info( "Group database initialized." );
+        }
+        catch( ClassNotFoundException e )
+        {
+            log.error( "GroupDatabase class " + dbClassName + " cannot be found.", e );
+            dbInstantiationError = "Failed to locate GroupDatabase class " + dbClassName;
+            cause = e;
+        }
+        catch( InstantiationException e )
+        {
+            log.error( "GroupDatabase class " + dbClassName + " cannot be created.", e );
+            dbInstantiationError = "Failed to create GroupDatabase class " + dbClassName;
+            cause = e;
+        }
+        catch( IllegalAccessException e )
+        {
+            log.error( "You are not allowed to access group database class " + dbClassName + ".", e );
+            dbInstantiationError = "Access GroupDatabase class " + dbClassName + " denied";
+            cause = e;
+        }
+        catch( NoRequiredPropertyException e )
+        {
+            log.error( "Missing property: " + e.getMessage() + "." );
+            dbInstantiationError = "Missing property: " + e.getMessage();
+            cause = e;
+        }
+
+        if( dbInstantiationError != null )
+        {
+            throw new WikiSecurityException( dbInstantiationError + " Cause: " + (cause != null ? cause.getMessage() : "") );
+        }
+
+        return m_groupDatabase;
+    }
+
+    /**
+     * Returns an array of GroupPrincipals this GroupManager knows about. This
+     * method will return an array of GroupPrincipal objects corresponding to
+     * the wiki groups managed by this class. This method actually returns a
+     * defensive copy of an internally stored hashmap.
+     * @return an array of Principals representing the roles
+     */
+    public final Principal[] getRoles()
+    {
+        return m_groups.keySet().toArray( new Principal[m_groups.size()] );
+    }
+
+    /**
+     * Initializes the group cache by initializing the group database and
+     * obtaining a list of all of the groups it stores.
+     * @param engine the wiki engine
+     * @param props the properties used to initialize the wiki engine
+     * @see GroupDatabase#initialize(com.ecyrd.jspwiki.WikiEngine,
+     *      java.util.Properties)
+     * @see GroupDatabase#groups()
+     * @throws WikiSecurityException if GroupManager cannot be initialized
+     */
+    public final void initialize( WikiEngine engine, Properties props ) throws WikiSecurityException
+    {
+        m_engine = engine;
+
+        try
+        {
+            m_groupDatabase = getGroupDatabase();
+        }
+        catch ( WikiException e )
+        {
+            throw new WikiSecurityException( e.getMessage() );
+        }
+
+        // Load all groups from the database into the cache
+        Group[] groups = m_groupDatabase.groups();
+        synchronized( m_groups )
+        {
+            for( int i = 0; i < groups.length; i++ )
+            {
+                Group group = groups[i];
+                // Add new group to cache; fire GROUP_ADD event
+                m_groups.put( group.getPrincipal(), group );
+                fireEvent( WikiSecurityEvent.GROUP_ADD, group );
+            }
+        }
+
+        // Make the GroupManager listen for WikiEvents (WikiSecurityEvents for changed user profiles)
+        engine.getUserManager().addWikiEventListener( this );
+
+        // Success!
+        log.info( "Authorizer GroupManager initialized successfully; loaded " + groups.length + " group(s)." );
+
+    }
+
+    /**
+     * <p>
+     * Determines whether the Subject associated with a WikiSession is in a
+     * particular role. This method takes two parameters: the WikiSession
+     * containing the subject and the desired role ( which may be a Role or a
+     * Group). If either parameter is <code>null</code>, or if the user is
+     * not authenticated, this method returns <code>false</code>.
+     * </p>
+     * <p>
+     * With respect to this implementation, the supplied Principal must be a
+     * GroupPrincipal. The Subject posesses the "role" if it the session is
+     * authenticated <em>and</em> a Subject's principal is a member of the
+     * corresponding Group. This method simply finds the Group in question, then
+     * delegates to {@link Group#isMember(Principal)} for each of the principals
+     * in the Subject's principal set.
+     * </p>
+     * @param session the current WikiSession
+     * @param role the role to check
+     * @return <code>true</code> if the user is considered to be in the role,
+     *         <code>false</code> otherwise
+     */
+    public final boolean isUserInRole( WikiSession session, Principal role )
+    {
+        // Always return false if session/role is null, or if
+        // role isn't a GroupPrincipal
+        if ( session == null || role == null || !( role instanceof GroupPrincipal ) || !session.isAuthenticated() )
+        {
+            return false;
+        }
+
+        // Get the group we're examining
+        Group group = m_groups.get( role );
+        if ( group == null )
+        {
+            return false;
+        }
+
+        // Check each user principal to see if it belongs to the group
+        Principal[] principals = session.getPrincipals();
+        for ( int i = 0; i < principals.length; i++ )
+        {
+            if ( AuthenticationManager.isUserPrincipal( principals[i] ) && group.isMember( principals[i] ) )
+            {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * <p>
+     * Extracts group name and members from passed parameters and populates an
+     * existing Group with them. The Group will either be a copy of an existing
+     * Group (if one can be found), or a new, unregistered Group (if not).
+     * Optionally, this method can throw a WikiSecurityException if the Group
+     * does not yet exist in the GroupManager cache.
+     * </p>
+     * <p>
+     * The <code>group</code> parameter in the HTTP request contains the Group
+     * name to look up and populate. The <code>members</code> parameter
+     * contains the member list. If these differ from those in the existing
+     * group, the passed values override the old values.
+     * </p>
+     * <p>
+     * This method does not commit the new Group to the GroupManager cache. To
+     * do that, use {@link #setGroup(WikiSession, Group)}.
+     * </p>
+     * @param name the name of the group to construct
+     * @param memberLine the line of text containing the group membership list
+     * @param create whether this method should create a new, empty Group if one
+     *            with the requested name is not found. If <code>false</code>,
+     *            groups that do not exist will cause a
+     *            <code>NoSuchPrincipalException</code> to be thrown
+     * @return a new, populated group
+     * @see com.ecyrd.jspwiki.auth.authorize.Group#RESTRICTED_GROUPNAMES
+     * @throws WikiSecurityException if the group name isn't allowed, or if
+     * <code>create</code> is <code>false</code>
+     * and the Group named <code>name</code> does not exist
+     */
+    public final Group parseGroup( String name, String memberLine, boolean create ) throws WikiSecurityException
+    {
+        // If null name parameter, it's because someone's creating a new group
+        if ( name == null )
+        {
+            if ( create )
+            {
+                name = "MyGroup";
+            }
+            else
+            {
+                throw new WikiSecurityException( "Group name cannot be blank." );
+            }
+        }
+        else if ( ArrayUtils.contains( Group.RESTRICTED_GROUPNAMES, name ) )
+        {
+            // Certain names are forbidden
+            throw new WikiSecurityException( "Illegal group name: " + name );
+        }
+        name = name.trim();
+
+        // Normalize the member line
+        if ( InputValidator.isBlank( memberLine ) )
+        {
+            memberLine = "";
+        }
+        memberLine = memberLine.trim();
+
+        // Create or retrieve the group (may have been previously cached)
+        Group group = new Group( name, m_engine.getApplicationName() );
+        try
+        {
+            Group existingGroup = getGroup( name );
+
+            // If existing, clone it
+            group.setCreator( existingGroup.getCreator() );
+            group.setCreated( existingGroup.getCreated() );
+            group.setModifier( existingGroup.getModifier() );
+            group.setLastModified( existingGroup.getLastModified() );
+            List<Principal> existingMembers = existingGroup.getMembers();
+            for( Principal member : existingMembers )
+            {
+                group.add( member );
+            }
+        }
+        catch( NoSuchPrincipalException e )
+        {
+            // It's a new group.... throw error if we don't create new ones
+            if ( !create )
+            {
+                throw new NoSuchPrincipalException( "Group '" + name + "' does not exist." );
+            }
+        }
+
+        // If passed members not empty, overwrite
+        String[] members = extractMembers( memberLine );
+        if ( members.length > 0 )
+        {
+            group.clear();
+            for( int i = 0; i < members.length; i++ )
+            {
+                group.add( new WikiPrincipal( members[i] ) );
+            }
+        }
+
+        return group;
+    }
+
+    /**
+     * <p>
+     * Extracts group name and members from the HTTP request and populates an
+     * existing Group with them. The Group will either be a copy of an existing
+     * Group (if one can be found), or a new, unregistered Group (if not).
+     * Optionally, this method can throw a WikiSecurityException if the Group
+     * does not yet exist in the GroupManager cache.
+     * </p>
+     * <p>
+     * The <code>group</code> parameter in the HTTP request contains the Group
+     * name to look up and populate. The <code>members</code> parameter
+     * contains the member list. If these differ from those in the existing
+     * group, the passed values override the old values.
+     * </p>
+     * <p>
+     * This method does not commit the new Group to the GroupManager cache. To
+     * do that, use {@link #setGroup(WikiSession, Group)}.
+     * </p>
+     * @param context the current wiki context
+     * @param create whether this method should create a new, empty Group if one
+     *            with the requested name is not found. If <code>false</code>,
+     *            groups that do not exist will cause a
+     *            <code>NoSuchPrincipalException</code> to be thrown
+     * @return a new, populated group
+     * @throws WikiSecurityException if the group name isn't allowed, or if
+     * <code>create</code> is <code>false</code>
+     * and the Group does not exist
+     */
+    public final Group parseGroup( WikiContext context, boolean create ) throws WikiSecurityException
+    {
+        // Extract parameters
+        HttpServletRequest request = context.getHttpRequest();
+        String name = request.getParameter( "group" );
+        String memberLine = request.getParameter( "members" );
+
+        // Create the named group; we pass on any NoSuchPrincipalExceptions
+        // that may be thrown if create == false, or WikiSecurityExceptions
+        Group group = parseGroup( name, memberLine, create );
+
+        // If no members, add the current user by default
+        if ( group.getMembers().size() == 0 )
+        {
+            group.add( context.getWikiSession().getUserPrincipal() );
+        }
+
+        return group;
+    }
+
+    /**
+     * Removes a named Group from the group database. If not found, throws a
+     * <code>NoSuchPrincipalException</code>. After removal, this method will
+     * commit the delete to the back-end group database. It will also fire a
+     * {@link com.ecyrd.jspwiki.event.WikiSecurityEvent#GROUP_REMOVE} event with
+     * the GroupManager instance as the source and the Group as target.
+     * If <code>index</code> is <code>null</code>, this method throws
+     * an {@link IllegalArgumentException}.
+     * @param index the group to remove
+     * @throws WikiSecurityException if the Group cannot be removed by
+     * the back-end
+     * @see com.ecyrd.jspwiki.auth.authorize.GroupDatabase#delete(Group)
+     */
+    public final void removeGroup( String index ) throws WikiSecurityException
+    {
+        if ( index == null )
+        {
+            throw new IllegalArgumentException( "Group cannot be null." );
+        }
+
+        Group group = m_groups.get( new GroupPrincipal( index ) );
+        if ( group == null )
+        {
+            throw new NoSuchPrincipalException( "Group " + index + " not found" );
+        }
+
+        // Delete the group
+        // TODO: need rollback procedure
+        synchronized( m_groups )
+        {
+            m_groups.remove( group.getPrincipal() );
+        }
+        m_groupDatabase.delete( group );
+        fireEvent( WikiSecurityEvent.GROUP_REMOVE, group );
+    }
+
+    /**
+     * <p>
+     * Saves the {@link Group} created by a user in a wiki session. This method
+     * registers the Group with the GroupManager and saves it to the back-end
+     * database. If an existing Group with the same name already exists, the new
+     * group will overwrite it. After saving the Group, the group database
+     * changes are committed.
+     * </p>
+     * <p>
+     * This method fires the following events:
+     * </p>
+     * <ul>
+     * <li><strong>When creating a new Group</strong>, this method fires a
+     * {@link com.ecyrd.jspwiki.event.WikiSecurityEvent#GROUP_ADD} with the
+     * GroupManager instance as its source and the new Group as the target.</li>
+     * <li><strong>When overwriting an existing Group</strong>, this method
+     * fires a new {@link com.ecyrd.jspwiki.event.WikiSecurityEvent#GROUP_REMOVE}
+     * with this GroupManager instance as the source, and the new Group as the
+     * target. It then fires a
+     * {@link com.ecyrd.jspwiki.event.WikiSecurityEvent#GROUP_ADD} event with the
+     * same source and target.</li>
+     * </ul>
+     * <p>
+     * In addition, if the save or commit actions fail, this method will attempt
+     * to restore the older version of the wiki group if it exists. This will
+     * result in a <code>GROUP_REMOVE</code> event (for the new version of the
+     * Group) followed by a <code>GROUP_ADD</code> event (to indicate
+     * restoration of the old version).
+     * </p>
+     * <p>
+     * This method will register the new Group with the GroupManager. For example,
+     * {@link com.ecyrd.jspwiki.auth.AuthenticationManager} attaches each
+     * WikiSession as a GroupManager listener. Thus, the act of registering a
+     * Group with <code>setGroup</code> means that all WikiSessions will
+     * automatically receive group add/change/delete events immediately.
+     * </p>
+     * @param session the wiki session, which may not be <code>null</code>
+     * @param group the Group, which may not be <code>null</code>
+     * @throws WikiSecurityException if the Group cannot be saved by the back-end
+     */
+    public final void setGroup( WikiSession session, Group group ) throws WikiSecurityException
+    {
+        // TODO: check for appropriate permissions
+
+        // If group already exists, delete it; fire GROUP_REMOVE event
+        Group oldGroup = m_groups.get( group.getPrincipal() );
+        if ( oldGroup != null )
+        {
+            fireEvent( WikiSecurityEvent.GROUP_REMOVE, oldGroup );
+            synchronized( m_groups )
+            {
+                m_groups.remove( oldGroup.getPrincipal() );
+            }
+        }
+
+        // Copy existing modifier info & timestamps
+        if ( oldGroup != null )
+        {
+            group.setCreator( oldGroup.getCreator() );
+            group.setCreated( oldGroup.getCreated() );
+            group.setModifier( oldGroup.getModifier() );
+            group.setLastModified( oldGroup.getLastModified() );
+        }
+
+        // Add new group to cache; announce GROUP_ADD event
+        synchronized( m_groups )
+        {
+            m_groups.put( group.getPrincipal(), group );
+        }
+        fireEvent( WikiSecurityEvent.GROUP_ADD, group );
+
+        // Save the group to back-end database; if it fails,
+        // roll back to previous state. Note that the back-end
+        // MUST timestammp the create/modify fields in the Group.
+        try
+        {
+            m_groupDatabase.save( group, session.getUserPrincipal() );
+        }
+
+        // We got an exception! Roll back...
+        catch( WikiSecurityException e )
+        {
+            if ( oldGroup != null )
+            {
+                // Restore previous version, re-throw...
+                fireEvent( WikiSecurityEvent.GROUP_REMOVE, group );
+                fireEvent( WikiSecurityEvent.GROUP_ADD, oldGroup );
+                synchronized( m_groups )
+                {
+                    m_groups.put( oldGroup.getPrincipal(), oldGroup );
+                }
+                throw new WikiSecurityException( e.getMessage() + " (rolled back to previous version)." );
+            }
+            // Re-throw security exception
+            throw new WikiSecurityException( e.getMessage() );
+        }
+    }
+
+    /**
+     * Validates a Group, and appends any errors to the session errors list. Any
+     * validation errors are added to the wiki session's messages collection
+     * (see {@link WikiSession#getMessages()}.
+     * @param context the current wiki context
+     * @param group the supplied Group
+     */
+    public final void validateGroup( WikiContext context, Group group )
+    {
+        WikiSession session = context.getWikiSession();
+        InputValidator validator = new InputValidator( MESSAGES_KEY, session );
+
+        // Name cannot be null or one of the restricted names
+        try
+        {
+            checkGroupName( session, group.getName() );
+        }
+        catch( WikiSecurityException e )
+        {
+
+        }
+
+        // Member names must be "safe" strings
+        for( Principal member: group.getMembers() )
+        {
+            validator.validateNotNull( member.getName(), "Full name", InputValidator.ID );
+        }
+    }
+
+    /**
+     * Extracts carriage-return separated members into a Set of String objects.
+     * @param memberLine the list of members
+     * @return the list of members
+     */
+    protected final String[] extractMembers( String memberLine )
+    {
+        Set<String> members = new HashSet<String>();
+        if ( memberLine != null )
+        {
+            StringTokenizer tok = new StringTokenizer( memberLine, "\n" );
+            while( tok.hasMoreTokens() )
+            {
+                String uid = tok.nextToken().trim();
+                if ( uid != null && uid.length() > 0 )
+                {
+                    members.add( uid );
+                }
+            }
+        }
+        return members.toArray( new String[members.size()] );
+    }
+
+    /**
+     * Checks if a String is blank or a restricted Group name, and if it is,
+     * appends an error to the WikiSession's message list.
+     * @param session the wiki session
+     * @param name the Group name to test
+     * @throws WikiSecurityException if <code>session</code> is
+     * <code>null</code> or the Group name is illegal
+     * @see Group#RESTRICTED_GROUPNAMES
+     */
+    protected final void checkGroupName( WikiSession session, String name ) throws WikiSecurityException
+    {
+        //TODO: groups cannot have the same name as a user
+
+        if( session == null )
+        {
+            throw new WikiSecurityException( "Session cannot be null." );
+        }
+
+        // Name cannot be null
+        InputValidator validator = new InputValidator( MESSAGES_KEY, session );
+        validator.validateNotNull( name, "Group name" );
+
+        // Name cannot be one of the restricted names either
+        if( ArrayUtils.contains( Group.RESTRICTED_GROUPNAMES, name ) )
+        {
+            throw new WikiSecurityException( "The group name '" + name + "' is illegal. Choose another." );
+        }
+    }
+
+
+    // 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 WikiSecurityEvent of the provided type, Principal and target Object
+     *  to all registered listeners.
+     *
+     * @see com.ecyrd.jspwiki.event.WikiSecurityEvent
+     * @param type       the event type to be fired
+     * @param target     the changed Object, which may be <code>null</code>
+     */
+    protected final void fireEvent( int type, Object target )
+    {
+        if ( WikiEventManager.isListening(this) )
+        {
+            WikiEventManager.fireEvent(this,new WikiSecurityEvent(this,type,target));
+        }
+    }
+
+    /**
+     * Listens for {@link com.ecyrd.jspwiki.event.WikiSecurityEvent#PROFILE_NAME_CHANGED}
+     * events. If a user profile's name changes, each group is inspected. If an entry contains
+     * a name that has changed, it is replaced with the new one. No group events are emitted
+     * as a consequence of this method, because the group memberships are still the same; it is
+     * only the representations of the names within that are changing.
+     * @param event the incoming event
+     */
+    public void actionPerformed(WikiEvent event)
+    {
+        if (! ( event instanceof WikiSecurityEvent ) )
+        {
+            return;
+        }
+
+        WikiSecurityEvent se = (WikiSecurityEvent)event;
+        if ( se.getType() == WikiSecurityEvent.PROFILE_NAME_CHANGED )
+        {
+            WikiSession session = (WikiSession)se.getSource();
+            UserProfile[] profiles = (UserProfile[])se.getTarget();
+            Principal[] oldPrincipals = new Principal[] {
+                new WikiPrincipal( profiles[0].getLoginName() ),
+                new WikiPrincipal( profiles[0].getFullname() ),
+                new WikiPrincipal( profiles[0].getWikiName() ) };
+            Principal newPrincipal = new WikiPrincipal( profiles[1].getFullname() );
+
+            // Examine each group
+            int groupsChanged = 0;
+            try
+            {
+                Group[] groups = m_groupDatabase.groups();
+                for ( int i = 0; i < groups.length; i++ )
+                {
+                    boolean groupChanged = false;
+                    Group group = groups[i];
+                    for ( int j = 0; j < oldPrincipals.length; j++ )
+                    {
+                        if ( group.isMember( oldPrincipals[j] ) )
+                        {
+                            group.remove( oldPrincipals[j] );
+                            group.add( newPrincipal );
+                            groupChanged = true;
+                        }
+                    }
+                    if ( groupChanged )
+                    {
+                        setGroup( session, group );
+                        groupsChanged++;
+                    }
+                }
+            }
+            catch ( WikiException e )
+            {
+                // Oooo! This is really bad...
+                log.error( "Could not change user name in Group lists because of GroupDatabase error:" + e.getMessage() );
+            }
+            log.info( "Profile name change for '" + newPrincipal.toString() +
+                      "' caused " + groupsChanged + " groups to change also." );
+        }
+    }
+
+}

Added: incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/auth/authorize/JDBCGroupDatabase.java
URL: http://svn.apache.org/viewvc/incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/auth/authorize/JDBCGroupDatabase.java?rev=627255&view=auto
==============================================================================
--- incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/auth/authorize/JDBCGroupDatabase.java (added)
+++ incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/auth/authorize/JDBCGroupDatabase.java Tue Feb 12 21:53:55 2008
@@ -0,0 +1,621 @@
+/*
+    JSPWiki - a JSP-based WikiWiki clone.
+
+    Copyright (C) 2001-2007 JSPWiki Development Group
+
+    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.auth.authorize;
+
+import java.security.Principal;
+import java.sql.*;
+import java.util.*;
+import java.util.Date;
+
+import javax.naming.Context;
+import javax.naming.InitialContext;
+import javax.naming.NamingException;
+import javax.sql.DataSource;
+
+import org.apache.log4j.Logger;
+
+import com.ecyrd.jspwiki.NoRequiredPropertyException;
+import com.ecyrd.jspwiki.WikiEngine;
+import com.ecyrd.jspwiki.auth.NoSuchPrincipalException;
+import com.ecyrd.jspwiki.auth.WikiPrincipal;
+import com.ecyrd.jspwiki.auth.WikiSecurityException;
+
+/**
+ * <p>Implementation of GroupDatabase that persists {@link Group}
+ * objects to a JDBC DataSource, as might typically be provided by a web
+ * container. This implementation looks up the JDBC DataSource using JNDI.
+ * The JNDI name of the datasource, backing table and mapped columns used
+ * by this class are configured via settings in <code>jspwiki.properties</code>.</p>
+ * <p>Configurable properties are these:</p>
+ * <table>
+ *   <tr>
+ *   <thead>
+ *     <th>Property</th>
+ *     <th>Default</th>
+ *     <th>Definition</th>
+ *   <thead>
+ *   </tr>
+ *   <tr>
+ *     <td><code>jspwiki.groupdatabase.datasource</code></td>
+ *     <td><code>jdbc/GroupDatabase</code></td>
+ *     <td>The JNDI name of the DataSource</td>
+ *   </tr>
+ *   <tr>
+ *     <td><code>jspwiki.groupdatabase.table</code></td>
+ *     <td><code>groups</code></td>
+ *     <td>The table that stores the groups</td>
+ *   </tr>
+ *   <tr>
+ *     <td><code>jspwiki.groupdatabase.membertable</code></td>
+ *     <td><code>group_members</code></td>
+ *     <td>The table that stores the names of group members</td>
+ *   </tr>
+ *   <tr>
+ *     <td><code>jspwiki.groupdatabase.created</code></td>
+ *     <td><code>created</code></td>
+ *     <td>The column containing the group's creation timestamp</td>
+ *   </tr>
+ *   <tr>
+ *     <td><code>jspwiki.groupdatabase.creator</code></td>
+ *     <td><code>creator</code></td>
+ *     <td>The column containing the group creator's name</td>
+ *   </tr>
+ *   <tr>
+ *     <td><code>jspwiki.groupdatabase.name</code></td>
+ *     <td><code>name</code></td>
+ *     <td>The column containing the group's name</td>
+ *   </tr>
+ *   <tr>
+ *     <td><code>jspwiki.groupdatabase.member</code></td>
+ *     <td><code>member</code></td>
+ *     <td>The column containing the group member's name</td>
+ *   </tr>
+ *   <tr>
+ *     <td><code>jspwiki.groupdatabase.modified</code></td>
+ *     <td><code>modified</code></td>
+ *     <td>The column containing the group's last-modified timestamp</td>
+ *   </tr>
+ *   <tr>
+ *     <td><code>jspwiki.groupdatabase.modifier</code></td>
+ *     <td><code>modifier</code></td>
+ *     <td>The column containing the name of the user who last modified the group</td>
+ *   </tr>
+ * </table>
+ * <p>This class is typically used in conjunction with a web container's JNDI resource
+ * factory. For example, Tomcat versions 4 and higher provide a basic JNDI factory
+ * for registering DataSources. To give JSPWiki access to the JNDI resource named
+ * by <code>jdbc/GroupDatabase</code>, you would declare the datasource resource similar to this:</p>
+ * <blockquote><code>&lt;Context ...&gt;<br/>
+ *  &nbsp;&nbsp;...<br/>
+ *  &nbsp;&nbsp;&lt;Resource name="jdbc/GroupDatabase" auth="Container"<br/>
+ *  &nbsp;&nbsp;&nbsp;&nbsp;type="javax.sql.DataSource" username="dbusername" password="dbpassword"<br/>
+ *  &nbsp;&nbsp;&nbsp;&nbsp;driverClassName="org.hsql.jdbcDriver" url="jdbc:HypersonicSQL:database"<br/>
+ *  &nbsp;&nbsp;&nbsp;&nbsp;maxActive="8" maxIdle="4"/&gt;<br/>
+ *  &nbsp;...<br/>
+ * &lt;/Context&gt;</code></blockquote>
+ * <p>JDBC driver JARs should be added to Tomcat's <code>common/lib</code> directory.
+ * For more Tomcat 5.5 JNDI configuration examples,
+ * see <a href="http://tomcat.apache.org/tomcat-5.5-doc/jndi-resources-howto.html">
+ * http://tomcat.apache.org/tomcat-5.5-doc/jndi-resources-howto.html</a>.</p>
+ * <p>JDBCGroupDatabase commits changes as transactions if the back-end database supports them.
+ * If the database supports transactions, group changes are saved
+ * to permanent storage only when the {@link #commit()} method is called. If the database does <em>not</em>
+ * support transactions, then changes are made immediately (during the {@link #save(Group, Principal)}
+ * method), and the {@linkplain #commit()} method no-ops. Thus, callers should always call the
+ * {@linkplain #commit()} method after saving a profile to guarantee that changes are applied.</p>
+ * @author Andrew R. Jaquith
+ * @since 2.3
+ */
+public class JDBCGroupDatabase implements GroupDatabase
+{
+     /** Default column name that stores the JNDI name of the DataSource. */
+    public static final String DEFAULT_GROUPDB_DATASOURCE   = "jdbc/GroupDatabase";
+    /** Default table name for the table that stores groups. */
+    public static final String DEFAULT_GROUPDB_TABLE        = "groups";
+    /** Default column name that stores the names of group members. */
+    public static final String DEFAULT_GROUPDB_MEMBER_TABLE = "group_members";
+    /** Default column name that stores the the group creation timestamps. */
+    public static final String DEFAULT_GROUPDB_CREATED      = "created";
+    /** Default column name that stores group creator names. */
+    public static final String DEFAULT_GROUPDB_CREATOR      = "creator";
+    /** Default column name that stores the group names. */
+    public static final String DEFAULT_GROUPDB_NAME         = "name";
+    /** Default column name that stores group member names. */
+    public static final String DEFAULT_GROUPDB_MEMBER       = "member";
+    /** Default column name that stores group last-modified timestamps. */
+    public static final String DEFAULT_GROUPDB_MODIFIED     = "modified";
+    /** Default column name that stores names of users who last modified groups. */
+    public static final String DEFAULT_GROUPDB_MODIFIER     = "modifier";
+
+    /** The JNDI name of the DataSource. */
+    public static final String PROP_GROUPDB_DATASOURCE   = "jspwiki.groupdatabase.datasource";
+    /** The table that stores the groups. */
+    public static final String PROP_GROUPDB_TABLE        = "jspwiki.groupdatabase.table";
+    /** The table that stores the names of group members. */
+    public static final String PROP_GROUPDB_MEMBER_TABLE = "jspwiki.groupdatabase.membertable";
+    /** The column containing the group's creation timestamp. */
+    public static final String PROP_GROUPDB_CREATED      = "jspwiki.groupdatabase.created";
+    /** The column containing the group creator's name. */
+    public static final String PROP_GROUPDB_CREATOR      = "jspwiki.groupdatabase.creator";
+    /** The column containing the group's name. */
+    public static final String PROP_GROUPDB_NAME         = "jspwiki.groupdatabase.name";
+    /** The column containing the group member's name. */
+    public static final String PROP_GROUPDB_MEMBER       = "jspwiki.groupdatabase.member";
+    /** The column containing the group's last-modified timestamp. */
+    public static final String PROP_GROUPDB_MODIFIED     = "jspwiki.groupdatabase.modified";
+    /** The column containing the name of the user who last modified the group. */
+    public static final String PROP_GROUPDB_MODIFIER     = "jspwiki.groupdatabase.modifier";
+
+    protected static final Logger log                     = Logger.getLogger( JDBCGroupDatabase.class );
+
+    private DataSource m_ds = null;
+    private String m_table = null;
+    private String m_memberTable = null;
+    private String m_created = null;
+    private String m_creator = null;
+    private String m_name = null;
+    private String m_member = null;
+    private String m_modified = null;
+    private String m_modifier = null;
+    private String m_findAll = null;
+    private String m_findGroup = null;
+    private String m_findMembers = null;
+    private String m_insertGroup = null;
+    private String m_insertGroupMembers = null;
+    private String m_updateGroup = null;
+    private String m_deleteGroup = null;
+    private String m_deleteGroupMembers = null;
+    private boolean m_supportsCommits = false;
+    private WikiEngine m_engine = null;
+
+    /**
+     * No-op method that in previous versions of JSPWiki was intended to
+     * atomically commit changes to the user database. Now, the
+     * {@link #save(Group, Principal)} and {@link #delete(Group)} methods
+     * are atomic themselves.
+     * @throws WikiSecurityException never...
+     * @deprecated there is no need to call this method because the save and
+     * delete methods contain their own commit logic
+     */
+    public void commit() throws WikiSecurityException
+    { }
+
+    /**
+     * Looks up and deletes a {@link Group} from the group database. If the
+     * group database does not contain the supplied Group. this method throws a
+     * {@link NoSuchPrincipalException}. The method commits the results
+     * of the delete to persistent storage.
+     * @param group the group to remove
+     * @throws WikiSecurityException if the database does not contain the
+     * supplied group (thrown as {@link NoSuchPrincipalException}) or if
+     * the commit did not succeed
+     */
+    public void delete( Group group ) throws WikiSecurityException
+    {
+        if ( !exists( group ) )
+        {
+            throw new NoSuchPrincipalException( "Not in database: " + group.getName() );
+        }
+
+        String groupName = group.getName();
+        Connection conn = null;
+        try
+        {
+            // Open the database connection
+            conn = m_ds.getConnection();
+            if ( m_supportsCommits )
+            {
+                conn.setAutoCommit( false );
+            }
+
+            PreparedStatement ps = conn.prepareStatement( m_deleteGroup );
+            ps.setString( 1, groupName);
+            ps.execute();
+            ps.close();
+            
+            ps = conn.prepareStatement( m_deleteGroupMembers );
+            ps.setString( 1, groupName);
+            ps.execute();
+            ps.close();
+
+            // Commit and close connection
+            if ( m_supportsCommits )
+            {
+                conn.commit();
+            }
+        }
+        catch ( SQLException e )
+        {
+            throw new WikiSecurityException( "Could not delete group " + groupName + ": " + e.getMessage() );
+        }
+        finally
+        {
+            try { conn.close(); } catch (Exception e) {}
+        }
+    }
+
+    /**
+     * Returns all wiki groups that are stored in the GroupDatabase as an array
+     * of Group objects. If the database does not contain any groups, this
+     * method will return a zero-length array. This method causes back-end
+     * storage to load the entire set of group; thus, it should be called
+     * infrequently (e.g., at initialization time).
+     * @return the wiki groups
+     * @throws WikiSecurityException if the groups cannot be returned by the back-end
+     */
+    public Group[] groups() throws WikiSecurityException
+    {
+        Set<Group> groups = new HashSet<Group>();
+        Connection conn = null;
+        try
+        {
+            // Open the database connection
+            conn = m_ds.getConnection();
+
+            PreparedStatement ps = conn.prepareStatement( m_findAll );
+            ResultSet rs = ps.executeQuery();
+            while ( rs.next() )
+            {
+                String groupName = rs.getString( m_name );
+                if ( groupName == null )
+                {
+                    log.warn( "Detected null group name in JDBCGroupDataBase. Check your group database." );
+                }
+                else
+                {
+                    Group group = new Group( groupName, m_engine.getApplicationName() );
+                    group.setCreated( rs.getTimestamp( m_created ) );
+                    group.setCreator( rs.getString( m_creator ) );
+                    group.setLastModified( rs.getTimestamp( m_modified ) );
+                    group.setModifier( rs.getString( m_modifier ) );
+                    populateGroup( group );
+                    groups.add( group );
+                }
+            }
+            ps.close();
+        }
+        catch ( SQLException e )
+        {
+            throw new WikiSecurityException( e.getMessage() );
+        }
+        finally
+        {
+            try { conn.close(); } catch (Exception e) {}
+        }
+
+        return groups.toArray( new Group[groups.size()] );
+    }
+
+    /**
+     * Saves a Group to the group database. Note that this method <em>must</em>
+     * fail, and throw an <code>IllegalArgumentException</code>, if the
+     * proposed group is the same name as one of the built-in Roles: e.g.,
+     * Admin, Authenticated, etc. The database is responsible for setting
+     * create/modify timestamps, upon a successful save, to the Group.
+     * The method commits the results of the delete to persistent storage.
+     * @param group the Group to save
+     * @param modifier the user who saved the Group
+     * @throws WikiSecurityException if the Group could not be saved successfully
+     */
+    public void save( Group group, Principal modifier ) throws WikiSecurityException
+    {
+        if ( group == null || modifier == null )
+        {
+            throw new IllegalArgumentException( "Group or modifier cannot be null." );
+        }
+
+        boolean exists = exists( group );
+        Connection conn = null;
+
+        try
+        {
+            // Open the database connection
+            conn = m_ds.getConnection();
+            if ( m_supportsCommits )
+            {
+                conn.setAutoCommit( false );
+            }
+
+            PreparedStatement ps;
+            Timestamp ts = new Timestamp( System.currentTimeMillis() );
+            Date modDate = new Date( ts.getTime() );
+            if ( !exists )
+            {
+                // Group is new: insert new group record
+                ps = conn.prepareStatement( m_insertGroup );
+                ps.setString( 1, group.getName() );
+                ps.setTimestamp( 2, ts );
+                ps.setString( 3, modifier.getName() );
+                ps.setTimestamp( 4, ts );
+                ps.setString( 5, modifier.getName() );
+                ps.execute();
+
+                // Set the group creation time
+                group.setCreated( modDate );
+                group.setCreator( modifier.getName() );
+                ps.close();
+            }
+            else
+            {
+                // Modify existing group record
+                ps = conn.prepareStatement( m_updateGroup );
+                ps.setTimestamp( 1, ts);
+                ps.setString( 2, modifier.getName() );
+                ps.setString( 3, group.getName() );
+                ps.execute();
+                ps.close();
+            }
+            // Set the group modified time
+            group.setLastModified( modDate );
+            group.setModifier( modifier.getName() );
+
+            // Now, update the group member list
+
+            // First, delete all existing member records
+            ps = conn.prepareStatement( m_deleteGroupMembers );
+            ps.setString( 1, group.getName() );
+            ps.execute();
+            ps.close();
+            
+            // Insert group member records
+            ps = conn.prepareStatement( m_insertGroupMembers );
+            for ( Principal member : group.getMembers() )
+            {
+                ps.setString( 1, group.getName() );
+                ps.setString( 2, member.getName() );
+                ps.execute();
+            }
+            ps.close();
+
+            // Commit and close connection
+            if ( m_supportsCommits )
+            {
+                conn.commit();
+            }
+        }
+        catch ( SQLException e )
+        {
+            throw new WikiSecurityException( e.getMessage() );
+        }
+        finally
+        {
+            try { conn.close(); } catch (Exception e) {}
+        }
+    }
+
+    /**
+     * Initializes the group database based on values from a Properties object.
+     * @param engine the wiki engine
+     * @param props the properties used to initialize the group database
+     * @throws WikiSecurityException if the database could not be initialized successfully
+     * @throws NoRequiredPropertyException if a required property is not present
+     */
+    public void initialize( WikiEngine engine, Properties props ) throws NoRequiredPropertyException, WikiSecurityException
+    {
+        m_engine = engine;
+
+        String jndiName = props.getProperty( PROP_GROUPDB_DATASOURCE, DEFAULT_GROUPDB_DATASOURCE );
+        try
+        {
+            Context initCtx = new InitialContext();
+            Context ctx = (Context) initCtx.lookup("java:comp/env");
+            m_ds = (DataSource) ctx.lookup( jndiName );
+
+            // Prepare the SQL selectors
+            m_table       = props.getProperty( PROP_GROUPDB_TABLE, DEFAULT_GROUPDB_TABLE );
+            m_memberTable = props.getProperty( PROP_GROUPDB_MEMBER_TABLE, DEFAULT_GROUPDB_MEMBER_TABLE );
+            m_name        = props.getProperty( PROP_GROUPDB_NAME, DEFAULT_GROUPDB_NAME );
+            m_created     = props.getProperty( PROP_GROUPDB_CREATED, DEFAULT_GROUPDB_CREATED );
+            m_creator     = props.getProperty( PROP_GROUPDB_CREATOR, DEFAULT_GROUPDB_CREATOR );
+            m_modifier    = props.getProperty( PROP_GROUPDB_MODIFIER, DEFAULT_GROUPDB_MODIFIER );
+            m_modified    = props.getProperty( PROP_GROUPDB_MODIFIED, DEFAULT_GROUPDB_MODIFIED );
+            m_member      = props.getProperty( PROP_GROUPDB_MEMBER, DEFAULT_GROUPDB_MEMBER );
+
+            m_findAll     = "SELECT DISTINCT * FROM " + m_table;
+            m_findGroup   = "SELECT DISTINCT * FROM " + m_table + " WHERE " + m_name + "=?";
+            m_findMembers = "SELECT * FROM " + m_memberTable + " WHERE " + m_name + "=?";
+
+            // Prepare the group insert/update SQL
+            m_insertGroup = "INSERT INTO " + m_table + " ("
+                              + m_name + ","
+                              + m_modified + ","
+                              + m_modifier + ","
+                              + m_created + ","
+                              + m_creator
+                              + ") VALUES (?,?,?,?,?)";
+            m_updateGroup = "UPDATE " + m_table + " SET "
+                              + m_modified + "=?,"
+                              + m_modifier + "=? WHERE " + m_name + "=?";
+
+            // Prepare the group member insert SQL
+            m_insertGroupMembers = "INSERT INTO " + m_memberTable + " ("
+                              + m_name + ","
+                              + m_member
+                              + ") VALUES (?,?)";
+
+            // Prepare the group delete SQL
+            m_deleteGroup = "DELETE FROM " + m_table + " WHERE " + m_name + "=?";
+            m_deleteGroupMembers = "DELETE FROM " + m_memberTable + " WHERE " + m_name + "=?";
+        }
+        catch( NamingException e )
+        {
+            log.error( "JDBCGroupDatabase initialization error: " + e.getMessage() );
+            throw new NoRequiredPropertyException( PROP_GROUPDB_DATASOURCE, "JDBCGroupDatabase initialization error: " + e.getMessage() );
+        }
+
+        // Test connection by doing a quickie select
+        Connection conn = null;
+        try
+        {
+            conn = m_ds.getConnection();
+            PreparedStatement ps = conn.prepareStatement( m_findAll );
+            ps.executeQuery();
+            ps.close();
+        }
+        catch ( SQLException e )
+        {
+            log.error( "JDBCGroupDatabase initialization error: " + e.getMessage() );
+            throw new NoRequiredPropertyException( PROP_GROUPDB_DATASOURCE, "JDBCGroupDatabase initialization error: " + e.getMessage() );
+        }
+        finally
+        {
+            try { conn.close(); } catch (Exception e) {}
+        }
+        log.info( "JDBCGroupDatabase initialized from JNDI DataSource: " + jndiName );
+
+        // Determine if the datasource supports commits
+        try
+        {
+            conn = m_ds.getConnection();
+            DatabaseMetaData dmd = conn.getMetaData();
+            if ( dmd.supportsTransactions() )
+            {
+                m_supportsCommits = true;
+                conn.setAutoCommit( false );
+                log.info("JDBCGroupDatabase supports transactions. Good; we will use them." );
+            }
+        }
+        catch ( SQLException e )
+        {
+            log.warn("JDBCGroupDatabase warning: user database doesn't seem to support transactions. Reason: " + e.getMessage() );
+            throw new NoRequiredPropertyException( PROP_GROUPDB_DATASOURCE, "JDBCGroupDatabase initialization error: " + e.getMessage() );
+        }
+        finally
+        {
+            try { conn.close(); } catch (Exception e) {}
+        }
+    }
+
+    /**
+     * Returns <code>true</code> if the Group exists in back-end storage.
+     * @param group the Group to look for
+     * @return the result of the search
+     */
+    private boolean exists( Group group )
+    {
+        String index = group.getName();
+        try
+        {
+            findGroup( index );
+            return true;
+        }
+        catch ( NoSuchPrincipalException e )
+        {
+            return false;
+        }
+    }
+
+    /**
+     * Loads and returns a Group from the back-end database matching a supplied name.
+     * @param index the name of the Group to find
+     * @return the populated Group
+     * @throws NoSuchPrincipalException if the Group cannot be found
+     * @throws SQLException if the database query returns an error
+     */
+    private Group findGroup( String index ) throws NoSuchPrincipalException
+    {
+        Group group = null;
+        boolean found = false;
+        boolean unique = true;
+        Connection conn = null;
+        try
+        {
+            // Open the database connection
+            conn = m_ds.getConnection();
+
+            PreparedStatement ps = conn.prepareStatement( m_findGroup );
+            ps.setString( 1, index );
+            ResultSet rs = ps.executeQuery();
+            while ( rs.next() )
+            {
+                if ( group != null )
+                {
+                    unique = false;
+                    break;
+                }
+                group = new Group( index, m_engine.getApplicationName() );
+                group.setCreated( rs.getTimestamp( m_created ) );
+                group.setCreator( rs.getString( m_creator ) );
+                group.setLastModified( rs.getTimestamp( m_modified ) );
+                group.setModifier( rs.getString( m_modifier ) );
+                populateGroup( group );
+                found = true;
+            }
+            ps.close();
+        }
+        catch ( SQLException e )
+        {
+            throw new NoSuchPrincipalException( e.getMessage() );
+        }
+        finally
+        {
+            try { conn.close(); } catch (Exception e) {}
+        }
+
+        if ( !found )
+        {
+            throw new NoSuchPrincipalException("Could not find group in database!");
+        }
+        if ( !unique )
+        {
+            throw new NoSuchPrincipalException("More than one group in database!");
+        }
+        return group;
+    }
+
+    /**
+     * Fills a Group with members.
+     * @param group the group to populate
+     * @return the populated Group
+     */
+    private Group populateGroup( Group group )
+    {
+        Connection conn = null;
+        try
+        {
+            // Open the database connection
+            conn = m_ds.getConnection();
+
+            PreparedStatement ps = conn.prepareStatement( m_findMembers );
+            ps.setString( 1, group.getName() );
+            ResultSet rs = ps.executeQuery();
+            while ( rs.next() )
+            {
+                String memberName = rs.getString( m_member );
+                if ( memberName != null )
+                {
+                    WikiPrincipal principal = new WikiPrincipal( memberName, WikiPrincipal.UNSPECIFIED );
+                    group.add( principal );
+                }
+            }
+            ps.close();
+        }
+        catch ( SQLException e )
+        {
+            // I guess that means there aren't any principals...
+        }
+        finally
+        {
+            try { conn.close(); } catch (Exception e) {}
+        }
+        return group;
+    }
+
+}

Added: incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/auth/authorize/Role.java
URL: http://svn.apache.org/viewvc/incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/auth/authorize/Role.java?rev=627255&view=auto
==============================================================================
--- incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/auth/authorize/Role.java (added)
+++ incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/auth/authorize/Role.java Tue Feb 12 21:53:55 2008
@@ -0,0 +1,128 @@
+/*
+ JSPWiki - a JSP-based WikiWiki clone.
+
+ Copyright (C) 2001-2004 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.auth.authorize;
+
+import java.security.Principal;
+
+/**
+ * A lightweight, immutable Principal that represents a built-in wiki role such
+ * as Anonymous, Asserted and Authenticated. It can also represent dynamic roles
+ * used by an external {@link com.ecyrd.jspwiki.auth.Authorizer}, such as a web
+ * container.
+ * @author Andrew Jaquith
+ * @since 2.3
+ */
+public final class Role implements Principal
+{
+
+    /** All users, regardless of authentication status */
+    public static final Role ALL           = new Role( "All" );
+
+    /** If the user hasn't supplied a name */
+    public static final Role ANONYMOUS     = new Role( "Anonymous" );
+
+    /** If the user has supplied a cookie with a username */
+    public static final Role ASSERTED      = new Role( "Asserted" );
+
+    /** If the user has authenticated with the Container or UserDatabase */
+    public static final Role AUTHENTICATED = new Role( "Authenticated" );
+
+    private final String   m_name;
+
+    /**
+     * Constructs a new Role with a given name.
+     * @param name the name of the Role
+     */
+    public Role( String name )
+    {
+        m_name = name;
+    }
+
+    /**
+     * Returns <code>true</code> if a supplied Role is a built-in Role:
+     * {@link #ALL}, {@link #ANONYMOUS}, {@link #ASSERTED},
+     * or {@link #AUTHENTICATED}.
+     * @param role the role to check
+     * @return the result of the check
+     */
+    public static final boolean isBuiltInRole(Role role)
+    {
+        return  role.equals( ALL ) || role.equals( ANONYMOUS ) ||
+                role.equals( ASSERTED ) || role.equals( AUTHENTICATED );
+
+    }
+
+    /**
+     * Returns <code>true</code> if the supplied name is identical to the name
+     * of a built-in Role; that is, the value returned by <code>getName()</code>
+     * for built-in Roles {@link #ALL}, {@link #ANONYMOUS},
+     * {@link #ASSERTED}, or {@link #AUTHENTICATED}.
+     * @param name the name to be tested
+     * @return <code>true</code> if the name is reserved; <code>false</code>
+     *         if not
+     */
+    public static final boolean isReservedName(String name)
+    {
+        return  name.equals( ALL.m_name ) ||
+                name.equals( ANONYMOUS.m_name ) || name.equals( ASSERTED.m_name ) ||
+                name.equals( AUTHENTICATED.m_name );
+    }
+
+    /**
+     * Returns a unique hashcode for the Role.
+     * @return the hashcode
+     */
+    public final int hashCode()
+    {
+        return m_name.hashCode();
+    }
+
+    /**
+     * Two Role objects are considered equal if their names are identical.
+     * @param obj the object to test
+     * @return <code>true</code> if both objects are of type Role and have identical names
+     * @see java.lang.Object#equals(java.lang.Object)
+     */
+    public final boolean equals( Object obj )
+    {
+        if ( obj == null || !( obj instanceof Role ) ) return false;
+        return m_name.equals( ( (Role) obj ).getName() );
+    }
+
+    /**
+     *  Returns the name of the Principal.
+     * @return the name of the Role
+     */
+    public final String getName()
+    {
+        return m_name;
+    }
+
+    /**
+     * Returns a String representation of the role
+     * @return the string representation of the role
+     * @see java.lang.Object#toString()
+     */
+    public final String toString()
+    {
+        return "[" + this.getClass().getName() + ": " + m_name + "]";
+    }
+
+}

Added: incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/auth/authorize/WebAuthorizer.java
URL: http://svn.apache.org/viewvc/incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/auth/authorize/WebAuthorizer.java?rev=627255&view=auto
==============================================================================
--- incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/auth/authorize/WebAuthorizer.java (added)
+++ incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/auth/authorize/WebAuthorizer.java Tue Feb 12 21:53:55 2008
@@ -0,0 +1,43 @@
+/*
+ * JSPWiki - a JSP-based WikiWiki clone. Copyright (C) 2001-2003 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.auth.authorize;
+
+import java.security.Principal;
+
+import javax.servlet.http.HttpServletRequest;
+
+import com.ecyrd.jspwiki.auth.Authorizer;
+
+/**
+ * Extends the {@link com.ecyrd.jspwiki.auth.Authorizer} interface by
+ * including a delgate method for 
+ * {@link javax.servlet.http.HttpServletRequest#isUserInRole(String)}.
+ * @author Andrew Jaquith
+ */
+public interface WebAuthorizer extends Authorizer
+{
+    
+    /**
+     * Determines whether a user associated with an HTTP request possesses
+     * a particular role. This method simply delegates to 
+     * {@link javax.servlet.http.HttpServletRequest#isUserInRole(String)}
+     * by converting the Principal's name to a String.
+     * @param request the HTTP request
+     * @param role the role to check
+     * @return <code>true</code> if the user is considered to be in the role,
+     *         <code>false</code> otherwise
+     */
+    public boolean isUserInRole( HttpServletRequest request, Principal role );
+    
+}

Added: incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/auth/authorize/WebContainerAuthorizer.java
URL: http://svn.apache.org/viewvc/incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/auth/authorize/WebContainerAuthorizer.java?rev=627255&view=auto
==============================================================================
--- incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/auth/authorize/WebContainerAuthorizer.java (added)
+++ incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/auth/authorize/WebContainerAuthorizer.java Tue Feb 12 21:53:55 2008
@@ -0,0 +1,466 @@
+/*
+ * JSPWiki - a JSP-based WikiWiki clone. Copyright (C) 2001-2003 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.auth.authorize;
+
+import java.io.IOException;
+import java.net.URL;
+import java.security.Principal;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Properties;
+import java.util.Set;
+
+import javax.servlet.http.HttpServletRequest;
+
+import net.sourceforge.stripes.mock.MockServletContext;
+
+import org.apache.log4j.Logger;
+import org.jdom.Document;
+import org.jdom.Element;
+import org.jdom.Namespace;
+import org.jdom.JDOMException;
+import org.jdom.input.SAXBuilder;
+import org.jdom.xpath.XPath;
+import org.xml.sax.EntityResolver;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+
+import com.ecyrd.jspwiki.InternalWikiException;
+import com.ecyrd.jspwiki.WikiEngine;
+import com.ecyrd.jspwiki.WikiSession;
+
+/**
+ * Authorizes users by delegating role membership checks to the servlet
+ * container. In addition to implementing methods for the
+ * <code>Authorizer</code> interface, this class also provides a convenience
+ * method {@link #isContainerAuthorized()} that queries the web application
+ * descriptor to determine if the container manages authorization.
+ * @author Andrew Jaquith
+ * @since 2.3
+ */
+public class WebContainerAuthorizer implements WebAuthorizer
+{
+    private static final String J2EE_SCHEMA_24_NAMESPACE = "http://java.sun.com/xml/ns/j2ee";
+
+    protected static final Logger log                   = Logger.getLogger( WebContainerAuthorizer.class );
+
+    protected WikiEngine          m_engine;
+
+    /**
+     * A lazily-initialized array of Roles that the container knows about. These
+     * are parsed from JSPWiki's <code>web.xml</code> web application
+     * deployment descriptor. If this file cannot be read for any reason, the
+     * role list will be empty. This is a hack designed to get around the fact
+     * that we have no direct way of querying the web container about which
+     * roles it manages.
+     */
+    protected Role[]            m_containerRoles      = new Role[0];
+
+    /**
+     * Lazily-initialized boolean flag indicating whether the web container
+     * protects JSPWiki resources.
+     */
+    protected boolean           m_containerAuthorized = false;
+
+    private Document            m_webxml = null;
+
+    /**
+     * Constructs a new instance of the WebContainerAuthorizer class.
+     */
+    public WebContainerAuthorizer()
+    {
+        super();
+    }
+
+    /**
+     * Initializes the authorizer for.
+     * @param engine the current wiki engine
+     * @param props the wiki engine initialization properties
+     */
+    public void initialize( WikiEngine engine, Properties props )
+    {
+        m_engine = engine;
+        m_containerAuthorized = false;
+
+        // FIXME: Error handling here is not very verbose
+        try
+        {
+            m_webxml = getWebXml();
+            if ( m_webxml != null )
+            {
+                // Add the J2EE 2.4 schema namespace
+                m_webxml.getRootElement().setNamespace( Namespace.getNamespace( J2EE_SCHEMA_24_NAMESPACE ) );
+
+                m_containerAuthorized = isConstrained( "/Delete.jsp", Role.ALL )
+                        && isConstrained( "/Login.jsp", Role.ALL );
+            }
+            if ( m_containerAuthorized )
+            {
+                m_containerRoles = getRoles( m_webxml );
+                log.info( "JSPWiki is using container-managed authentication." );
+            }
+            else
+            {
+                log.info( "JSPWiki is using custom authentication." );
+            }
+        }
+        catch ( IOException e )
+        {
+            log.error("Initialization failed: ",e);
+            throw new InternalWikiException( e.getClass().getName()+": "+e.getMessage() );
+        }
+        catch ( JDOMException e )
+        {
+            log.error("Malformed XML in web.xml",e);
+            throw new InternalWikiException( e.getClass().getName()+": "+e.getMessage() );
+        }
+
+        if ( m_containerRoles.length > 0 )
+        {
+            String roles = "";
+            for( int i = 0; i < m_containerRoles.length; i++ )
+            {
+                roles = roles + m_containerRoles[i] + " ";
+            }
+            log.info( " JSPWiki determined the web container manages these roles: " + roles );
+        }
+        log.info( "Authorizer WebContainerAuthorizer initialized successfully." );
+    }
+
+    /**
+     * Determines whether a user associated with an HTTP request possesses
+     * a particular role. This method simply delegates to 
+     * {@link javax.servlet.http.HttpServletRequest#isUserInRole(String)}
+     * by converting the Principal's name to a String.
+     * @param request the HTTP request
+     * @param role the role to check
+     * @return <code>true</code> if the user is considered to be in the role,
+     *         <code>false</code> otherwise
+     */
+    public boolean isUserInRole( HttpServletRequest request, Principal role )
+    {
+        return request.isUserInRole( role.getName() );
+    }
+
+    /**
+     * Determines whether the Subject associated with a WikiSession is in a
+     * particular role. This method takes two parameters: the WikiSession
+     * containing the subject and the desired role ( which may be a Role or a
+     * Group). If either parameter is <code>null</code>, this method must
+     * return <code>false</code>.
+     * This method simply examines the WikiSession subject to see if it
+     * possesses the desired Principal. We assume that the method
+     * {@link com.ecyrd.jspwiki.auth.AuthenticationManager#login(HttpServletRequest)}
+     * previously executed at user login time, and that it has injected
+     * the role Principals that were in force at login time.
+     * This is definitely a hack,
+     * but it eliminates the need for WikiSession to keep dangling
+     * references to the last WikiContext hanging around, just
+     * so we can look up the HttpServletRequest.
+     *
+     * @param session the current WikiSession
+     * @param role the role to check
+     * @return <code>true</code> if the user is considered to be in the role,
+     *         <code>false</code> otherwise
+     * @see com.ecyrd.jspwiki.auth.Authorizer#isUserInRole(com.ecyrd.jspwiki.WikiSession, java.security.Principal)
+     */
+    public boolean isUserInRole( WikiSession session, Principal role )
+    {
+        if ( session == null || role == null )
+        {
+            return false;
+        }
+        return session.hasPrincipal( role );
+    }
+
+    /**
+     * Looks up and returns a Role Principal matching a given String. If the
+     * Role does not match one of the container Roles identified during
+     * initialization, this method returns <code>null</code>.
+     * @param role the name of the Role to retrieve
+     * @return a Role Principal, or <code>null</code>
+     * @see com.ecyrd.jspwiki.auth.Authorizer#initialize(WikiEngine, Properties)
+     */
+    public Principal findRole( String role )
+    {
+        for( int i = 0; i < m_containerRoles.length; i++ )
+        {
+            if ( m_containerRoles[i].getName().equals( role ) )
+            {
+                return m_containerRoles[i];
+            }
+        }
+        return null;
+    }
+
+    /**
+     * <p>
+     * Protected method that identifies whether a particular webapp URL is
+     * constrained to a particular Role. The resource is considered constrained
+     * if:
+     * </p>
+     * <ul>
+     * <li>the web application deployment descriptor contains a
+     * <code>security-constraint</code> with a child
+     * <code>web-resource-collection/url-pattern</code> element matching the
+     * URL, <em>and</em>:</li>
+     * <li>this constraint also contains an
+     * <code>auth-constraint/role-name</code> element equal to the supplied
+     * Role's <code>getName()</code> method. If the supplied Role is Role.ALL,
+     * it matches all roles</li>
+     * </ul>
+     * @param url the web resource
+     * @param role the role
+     * @return <code>true</code> if the resource is constrained to the role,
+     *         <code>false</code> otherwise
+     * @throws JDOMException if elements cannot be parsed correctly
+     */
+    public boolean isConstrained( String url, Role role ) throws JDOMException
+    {
+        Element root = m_webxml.getRootElement();
+        XPath xpath;
+        String selector;
+
+        // Get all constraints that have our URL pattern
+        // (Note the crazy j: prefix to denote the 2.4 j2ee schema)
+        selector = "//j:web-app/j:security-constraint[j:web-resource-collection/j:url-pattern=\"" + url + "\"]";
+        xpath = XPath.newInstance( selector );
+        xpath.addNamespace( "j", J2EE_SCHEMA_24_NAMESPACE );
+        List constraints = xpath.selectNodes( root );
+
+        // Get all constraints that match our Role pattern
+        selector = "//j:web-app/j:security-constraint[j:auth-constraint/j:role-name=\"" + role.getName() + "\"]";
+        xpath = XPath.newInstance( selector );
+        xpath.addNamespace( "j", J2EE_SCHEMA_24_NAMESPACE );
+        List roles = xpath.selectNodes( root );
+
+        // If we can't find either one, we must not be constrained
+        if ( constraints.size() == 0 )
+        {
+            return false;
+        }
+
+        // Shortcut: if the role is ALL, we are constrained
+        if ( role.equals( Role.ALL ) )
+        {
+            return true;
+        }
+
+        // If no roles, we must not be constrained
+        if ( roles.size() == 0 )
+        {
+            return false;
+        }
+
+        // If a constraint is contained in both lists, we must be constrained
+        for ( Iterator c = constraints.iterator(); c.hasNext(); )
+        {
+            Element constraint = (Element)c.next();
+            for ( Iterator r = roles.iterator(); r.hasNext(); )
+            {
+                Element roleConstraint = (Element)r.next();
+                if ( constraint.equals( roleConstraint ) )
+                {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Returns <code>true</code> if the web container is configured to protect
+     * certain JSPWiki resources by requiring authentication. Specifically, this
+     * method parses JSPWiki's web application descriptor (<code>web.xml</code>)
+     * and identifies whether the string representation of
+     * {@link com.ecyrd.jspwiki.auth.authorize.Role#AUTHENTICATED} is required
+     * to access <code>/Delete.jsp</code> and <code>LoginRedirect.jsp</code>.
+     * If the administrator has uncommented the large
+     * <code>&lt;security-constraint&gt;</code> section of <code>web.xml</code>,
+     * this will be true. This is admittedly an indirect way to go about it, but
+     * it should be an accurate test for default installations, and also in 99%
+     * of customized installs.
+     * @return <code>true</code> if the container protects resources,
+     *         <code>false</code> otherwise
+     */
+    public boolean isContainerAuthorized()
+    {
+        return m_containerAuthorized;
+    }
+
+    /**
+     * Returns an array of role Principals this Authorizer knows about.
+     * This method will return an array of Role objects corresponding to
+     * the logical roles enumerated in the <code>web.xml</code>.
+     * This method actually returns a defensive copy of an internally stored
+     * array.
+     * @return an array of Principals representing the roles
+     */
+    public Principal[] getRoles()
+    {
+        return m_containerRoles.clone();
+    }
+
+    /**
+     * Protected method that extracts the roles from JSPWiki's web application
+     * deployment descriptor. Each Role is constructed by using the String
+     * representation of the Role, for example
+     * <code>new Role("Administrator")</code>.
+     * @param webxml the web application deployment descriptor
+     * @return an array of Role objects
+     * @throws JDOMException if elements cannot be parsed correctly
+     */
+    protected Role[] getRoles( Document webxml ) throws JDOMException
+    {
+        Set<Role> roles = new HashSet<Role>();
+        Element root = webxml.getRootElement();
+
+        // Get roles referred to by constraints
+        String selector = "//j:web-app/j:security-constraint/j:auth-constraint/j:role-name";
+        XPath xpath = XPath.newInstance( selector );
+        xpath.addNamespace( "j", J2EE_SCHEMA_24_NAMESPACE );
+        List nodes = xpath.selectNodes( root );
+        for( Iterator it = nodes.iterator(); it.hasNext(); )
+        {
+            String role = ( (Element) it.next() ).getTextTrim();
+            roles.add( new Role( role ) );
+        }
+
+        // Get all defined roles
+        selector = "//j:web-app/j:security-role/j:role-name";
+        xpath = XPath.newInstance( selector );
+        xpath.addNamespace( "j", J2EE_SCHEMA_24_NAMESPACE );
+        nodes = xpath.selectNodes( root );
+        for( Iterator it = nodes.iterator(); it.hasNext(); )
+        {
+            String role = ( (Element) it.next() ).getTextTrim();
+            roles.add( new Role( role ) );
+        }
+
+        return roles.toArray( new Role[roles.size()] );
+    }
+
+    /**
+     * Returns an {@link org.jdom.Document} representing JSPWiki's web
+     * application deployment descriptor. The document is obtained by calling
+     * the servlet context's <code>getResource()</code> method and requesting
+     * <code>/WEB-INF/web.xml</code>. For non-servlet applications, this
+     * method calls this class'
+     * {@link ClassLoader#getResource(java.lang.String)} and requesting
+     * <code>WEB-INF/web.xml</code>.
+     * @return the descriptor
+     * @throws IOException if the deployment descriptor cannot be found or opened
+     * @throws JDOMException if the deployment descriptor cannot be parsed correctly
+     */
+    protected Document getWebXml() throws JDOMException, IOException
+    {
+        URL url;
+        SAXBuilder builder = new SAXBuilder();
+        builder.setValidation( false );
+        builder.setEntityResolver( new LocalEntityResolver() );
+        Document doc = null;
+        if ( m_engine.getServletContext() == null )
+        {
+            ClassLoader cl = WebContainerAuthorizer.class.getClassLoader();
+            url = cl.getResource( "WEB-INF/web.xml" );
+            if( url != null )
+                log.info( "Examining " + url.toExternalForm() );
+        }
+        else
+        {
+            url = m_engine.getServletContext().getResource( "/WEB-INF/web.xml" );
+            // Hack in case we're using Stripes mock servlet context
+            // See bug [STS-376] at http://stripesframework.org/jira/browse/STS-376. Will be fixed in Stripes 1.5.
+            if ( url == null && m_engine.getServletContext() instanceof MockServletContext )
+            {
+                url = m_engine.getServletContext().getResource( "WEB-INF/web.xml" );
+            }
+            if( url != null )
+                log.info( "Examining " + url.toExternalForm() );
+        }
+        if( url == null )
+            throw new IOException("Unable to find web.xml for processing.");
+
+        log.debug( "Processing web.xml at " + url.toExternalForm() );
+        doc = builder.build( url );
+        return doc;
+    }
+
+    /**
+     * <p>XML entity resolver that redirects resolution requests by JDOM, JAXP and
+     * other XML parsers to locally-cached copies of the resources. Local
+     * resources are stored in the <code>WEB-INF/dtd</code> directory.</p>
+     * <p>For example, Sun Microsystem's DTD for the webapp 2.3 specification is normally
+     * kept at <code>http://java.sun.com/dtd/web-app_2_3.dtd</code>. The
+     * local copy is stored at <code>WEB-INF/dtd/web-app_2_3.dtd</code>.</p>
+     * @author Andrew Jaquith
+     */
+    public class LocalEntityResolver implements EntityResolver
+    {
+        /**
+         * Returns an XML input source for a requested external resource by
+         * reading the resource instead from local storage. The local resource path
+         * is <code>WEB-INF/dtd</code>, plus the file name of the requested
+         * resource, minus the non-filename path information.
+         * @param publicId the public ID, such as
+         *            <code>-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN</code>
+         * @param systemId the system ID, such as
+         *            <code>http://java.sun.com/dtd/web-app_2_3.dtd</code>
+         * @return the InputSource containing the resolved resource
+         * @see org.xml.sax.EntityResolver#resolveEntity(java.lang.String,
+         *      java.lang.String)
+         * @throws SAXException if the resource cannot be resolved locally
+         * @throws IOException if the resource cannot be opened
+         */
+        public InputSource resolveEntity( String publicId, String systemId ) throws SAXException, IOException
+        {
+            String file = systemId.substring( systemId.lastIndexOf( '/' ) + 1 );
+            URL url;
+            if ( m_engine.getServletContext() == null )
+            {
+                ClassLoader cl = WebContainerAuthorizer.class.getClassLoader();
+                url = cl.getResource( "WEB-INF/dtd/" + file );
+            }
+            else
+            {
+                url = m_engine.getServletContext().getResource( "/WEB-INF/dtd/" + file );
+            }
+
+            if( url != null )
+            {
+                InputSource is = new InputSource( url.openStream() );
+                log.debug( "Resolved systemID=" + systemId + " using local file " + url );
+                return is;
+            }
+
+            //
+            //  Let's fall back to default behaviour of the container, and let's
+            //  also let the user know what is going on.  This caught me by surprise
+            //  while running JSPWiki on an unconnected laptop...
+            //
+            //  The DTD needs to be resolved and read because it contains things like
+            //  entity definitions...
+            //
+            log.info("Please note: There are no local DTD references in /WEB-INF/dtd/"+file+"; falling back to default behaviour."+
+                     " This may mean that the XML parser will attempt to connect to the internet to find the DTD."+
+                     " If you are running JSPWiki locally in an unconnected network, you might want to put the DTD files in place to avoid nasty UnknownHostExceptions.");
+
+
+            // Fall back to default behaviour
+            return null;
+        }
+    }
+
+}