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><Context ...><br/>
+ * ...<br/>
+ * <Resource name="jdbc/GroupDatabase" auth="Container"<br/>
+ * type="javax.sql.DataSource" username="dbusername" password="dbpassword"<br/>
+ * driverClassName="org.hsql.jdbcDriver" url="jdbc:HypersonicSQL:database"<br/>
+ * maxActive="8" maxIdle="4"/><br/>
+ * ...<br/>
+ * </Context></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><security-constraint></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;
+ }
+ }
+
+}