/********************************************************************************** * $URL: https://source.sakaiproject.org/svn/providers/trunk/federating/src/java/org/sakaiproject/provider/user/FilterUserDirectoryProvider.java $ * $Id: FilterUserDirectoryProvider.java 105079 2012-02-24 23:08:11Z ottenhoff@longsight.com $ *********************************************************************************** * * Copyright (c) 2006, 2007, 2008 The Sakai Foundation * * Licensed under the Educational Community License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.opensource.org/licenses/ECL-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * **********************************************************************************/ // package package org.sakaiproject.provider.user; // imports import java.security.SecureRandom; import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; import java.util.List; import java.util.Vector; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.sakaiproject.user.api.ExternalUserSearchUDP; import org.sakaiproject.user.api.User; import org.sakaiproject.user.api.UserDirectoryProvider; import org.sakaiproject.user.api.UserEdit; import org.sakaiproject.user.api.UserFactory; import org.sakaiproject.user.api.UsersShareEmailUDP; /** * <p> * Filter User Directory Provider, calls a configure provider, and if that fails * calls the next provider in the chain. It does this for all methods. If the * response is a boolean, then true stop processing, false continues processon. * It is the reponsibility of the injected user directory provider to ignore * those calls that have nothing to do with it, either by reference to the * session, or by refernce to something in the environment or request. To Use, * add one or more of these beans to Spring, in a chain, marking the first one * in the chain as the 'official' userdirectory provider used by Sakai. * Construct the chain by setting the next FilterUserDirectorProvider to the * nextProvider and the real User Directory Provider to myProvider eg * * <pre> * * <bean * id="org.sakaiproject.user.api.UserDirectoryProvider" * class="org.sakaiproject.provider.user.FilterUserDirectoryProvider" * init-method="init" destroy-method="destroy" singleton="true"> * <property name="myProvider"> * <ref bean="org.sakaiproject.user.api.UserDirectoryProvider.provider1" /> * </property> * <property name="nextProvider"> * <ref bean="org.sakaiproject.user.api.UserDirectoryProvider.chain1" /> * </property> * </bean> * <bean * id="org.sakaiproject.user.api.UserDirectoryProvider.chain1" * class="org.sakaiproject.provider.user.FilterUserDirectoryProvider" * init-method="init" destroy-method="destroy" singleton="true"> * <property name="myProvider"> * <ref bean="org.sakaiproject.user.api.UserDirectoryProvider.provider2" /> * </property> * </bean> * </pre> * * @author Ian Boston, Andrew Thornton, Daniel Parry, Raad * @version $Revision: 105079 $ */ public class FilterUserDirectoryProvider implements UserDirectoryProvider, ExternalUserSearchUDP, UsersShareEmailUDP { /** Our log (commons). */ private static Log m_logger = LogFactory.getLog(FilterUserDirectoryProvider.class); private static ThreadLocal authenticatedProvider = new ThreadLocal(); /********************************************************************************************************************************************************************************************************************************************************** * Dependencies and their setter methods *********************************************************************************************************************************************************************************************************************************************************/ // The spring injected user provider we call /** * The underlying UserDirectoryProvider */ private UserDirectoryProvider myProvider; /** * The Next directory provider in the chain */ private UserDirectoryProvider nextProvider; private Long providerID = null; /** * Final initialization, once all dependencies are set. */ public void init() { // Initialize the providerID as a random Long. SecureRandom is guaranteed to // to give a separate id, however its not entirely thread safe, so I've reused // the thread local. It gets thrown away on the first auth attempt, so the // secure random wont hand around in production. SecureRandom sr = (SecureRandom)authenticatedProvider.get(); if ( sr == null ) { sr = new SecureRandom(); authenticatedProvider.set(sr); } providerID = new Long(sr.nextLong()); try { m_logger.info("init() FILTER as "+providerID); } catch (Throwable t) { m_logger.info(".init(): ", t); } } // init /** * Returns to uninitialized state. You can use this method to release resources thet your Service allocated when Turbine shuts down. */ public void destroy() { m_logger.info("destroy()"); } // destroy /********************************************************************************************************************************************************************************************************************************************************** * UserDirectoryProvider implementation *********************************************************************************************************************************************************************************************************************************************************/ /** * Construct. */ public FilterUserDirectoryProvider() { } // Switch /** * Access a user object. Update the object with the information found. * * @param edit * The user object (id is set) to fill in. * @return true if the user object was found and information updated, false if not. */ public boolean getUser(UserEdit edit) { if (m_logger.isDebugEnabled()) { m_logger.debug("FUDP: getUser(" + edit.getId() + " eid:" + edit.getEid() + ") as "+providerID); m_logger.debug("FUDP: doing myProvider.getUser() as " + providerID); } if ( myProvider.getUser(edit) ) { return true; } else if ( nextProvider != null ) { if (m_logger.isDebugEnabled()) { m_logger.debug("FUDP: doing nextProvider.getUser() as " + providerID); } return nextProvider.getUser(edit); } return false; } // getUser /** * Access a collection of UserEdit objects; if the user is found, update the information, otherwise remove the UserEdit object from the collection. * @param users The UserEdit objects (with id set) to fill in or remove. */ public void getUsers(Collection users) { if (m_logger.isDebugEnabled()) { m_logger.debug("getUsers() size()=" + users.size() + " as "+providerID); } if (nextProvider != null) { // We need to be clever and wrap the collection GetUsersCollectionWrapper wrapper = new GetUsersCollectionWrapper(users); if (m_logger.isDebugEnabled()) { m_logger.debug("using wrapper on " + myProvider + " as " + providerID); } myProvider.getUsers(wrapper.firstPassCollection()); if (m_logger.isDebugEnabled()) { m_logger.debug("Passing myProvider removals collection to nextProvider size()=" + wrapper.secondPassCollection.size() + " as " + providerID); m_logger.debug("using second wrapper on " + nextProvider); } nextProvider.getUsers(wrapper.secondPassCollection()); if (m_logger.isDebugEnabled()) { m_logger.debug("Total number of removals from users collection=" + wrapper.realRemovals.size() + " as " + providerID); m_logger.debug("Applying Changes"); } wrapper.apply(); } else { myProvider.getUsers(users); } } private class GetUsersCollectionWrapper { Collection inner; public GetUsersCollectionWrapper(Collection c) { this.inner = c; } ArrayList secondPassCollection = new ArrayList(); ArrayList realRemovals = new ArrayList(); public Collection firstPassCollection() { return createStoreRemovalsCollection(inner, secondPassCollection); } public Collection secondPassCollection() { return createStoreRemovalsCollection(secondPassCollection, realRemovals); } public Collection createStoreRemovalsCollection(final Collection originals, final Collection removals) { return new Collection() { public boolean add(Object o) { throw new UnsupportedOperationException("UDP should not add to the collection passed into getUsers()"); } public boolean addAll(Collection c) { throw new UnsupportedOperationException("UDP should not add to the collection passed into getUsers()"); } public void clear() { for (Iterator it = originals.iterator(); it.hasNext();) { removals.add(it.next()); } } public boolean contains(Object o) { return originals.contains(o) && !removals.contains(o); } public boolean containsAll(Collection col) { for (Iterator it = removals.iterator(); it.hasNext();) { if (col.contains(it.next())) return false; } return originals.containsAll(col); } public boolean isEmpty() { return originals.isEmpty() || removals.containsAll(originals); } public Iterator iterator() { return new Iterator() { Iterator internal = originals.iterator(); Object next = internal.hasNext() ? internal.next() : null; Object current; public boolean hasNext() { return (next != null); } public Object next() { current = next; next = null; while (next == null && internal.hasNext()) { next = internal.next(); if (removals.contains(next)) { next = null; } } return current; } public void remove() { if (m_logger.isDebugEnabled()) { User u = (User) current; m_logger.debug("Removing object from internal collection :" + u.getEid()); } if (originals.contains(current) && !removals.contains(current)) { removals.add(current); } } }; } /* * If remove is called for object o */ public boolean remove(Object o) { if (m_logger.isDebugEnabled()) { User u = (User) o; m_logger.debug("Removing object from internal collection :" + u.getEid()); } if (originals.contains(o) && !removals.contains(o)) { removals.add(o); return true; } return false; } public boolean removeAll(Collection c) { boolean doneSomething = false; for (Iterator it = c.iterator(); it.hasNext() ;) { Object o = it.next(); if (m_logger.isDebugEnabled()) { User u = (User) o; m_logger.debug("Removing object from internal collection :" + u.getEid()); } if (originals.contains(o) && !removals.contains(o)) { removals.add(o); doneSomething = true; } } return doneSomething; } public boolean retainAll(Collection c) { boolean doneSomething = false; for (Iterator it = originals.iterator(); it.hasNext();) { Object o = it.next(); if (m_logger.isDebugEnabled()) { User u = (User) o; m_logger.debug("Removing object from internal collection :" + u.getEid()); } if (!c.contains(o) && !removals.contains(o)) { removals.add(o); doneSomething = true; } } return doneSomething; } public int size() { int i = 0; for (Iterator it = originals.iterator(); it.hasNext(); ) { if (!removals.contains(it.next())) { i++; } } return i; } public Object[] toArray() { return toList().toArray(); } private ArrayList toList() { ArrayList l = new ArrayList(); for (Iterator it = originals.iterator(); it.hasNext(); ) { Object o = it.next(); if (!removals.contains(o)) { l.add(o); } } return l; } public Object[] toArray(Object[] a) { return toList().toArray(a); } }; } public void apply() { for (Iterator it = inner.iterator(); it.hasNext();) { Object o = it.next(); if (m_logger.isDebugEnabled()) { User u = (User) o; m_logger.debug("Actually removing object from collection :" + u.getEid()); } if (realRemovals.contains(o)) { it.remove(); } } } } /** * Find a user object who has this email address. Update the object with the information found. * * @param email * The email address string. * @return true if the user object was found and information updated, false if not. */ public boolean findUserByEmail(UserEdit edit, String email) { if (m_logger.isDebugEnabled()) { m_logger.debug("findUserByEmail() edit.getId()=" +edit.getId() +" email=" + email + " as "+providerID); } if ( myProvider.findUserByEmail(edit,email) ) { return true; } else if ( nextProvider != null ) { return nextProvider.findUserByEmail(edit,email); } return false; } // findUserByEmail /** * Find all user objects which have this email address. * * @param email * The email address string. * @param factory * Use this factory's newUser() method to create all the UserEdit objects you populate and return in the return collection. * @return Collection (UserEdit) of user objects that have this email address, or an empty Collection if there are none. */ public Collection findUsersByEmail(String email, UserFactory factory) { Collection rv = new Vector(); if ( myProvider instanceof UsersShareEmailUDP ) { UsersShareEmailUDP ushare = (UsersShareEmailUDP) myProvider; if (m_logger.isDebugEnabled()) { m_logger.debug("myProvider Multiple lookup on "+email+" for "+ushare + " as " + providerID); } rv.addAll(ushare.findUsersByEmail(email,factory)); if (m_logger.isDebugEnabled()) { m_logger.debug("myProvider - got "+rv.size()+" matches" + " as " + providerID); } } else { UserEdit edit = factory.newUser(); if ( myProvider.findUserByEmail(edit,email) ) { if (m_logger.isDebugEnabled()) { m_logger.debug("myProvider - found user "+edit.getId()+" for "+email + " as " + providerID); } rv.add(edit); } } if ( nextProvider instanceof UsersShareEmailUDP) { UsersShareEmailUDP ushare = (UsersShareEmailUDP) nextProvider; if (m_logger.isDebugEnabled()) { m_logger.debug("nextProvider Multiple lookup on "+email+" for "+ushare + " as " + providerID); } rv.addAll(ushare.findUsersByEmail(email,factory)); if (m_logger.isDebugEnabled()) { m_logger.debug("nextProvider - got "+rv.size()+" matches" + " as " + providerID); } } else if ( nextProvider != null ) { UserEdit edit = factory.newUser(); if ( nextProvider.findUserByEmail(edit,email) ) { if (m_logger.isDebugEnabled()) { m_logger.debug("nextProvider - found user "+edit.getId()+" for "+email + " as " + providerID); } rv.add(edit); } } return rv; } /** * Authenticate a user / password. If the user edit exists it may be modified, and will be stored if... * * @param id * The user id. * @param edit * The UserEdit matching the id to be authenticated (and updated) if we have one. * @param password * The password. * @return true if authenticated, false if not. */ public boolean authenticateUser(String userId, UserEdit edit, String password) { if (m_logger.isDebugEnabled()) { m_logger.debug("authenticateUser() userId=" + userId + " as "+providerID); } authenticatedProvider.set(null); if ( myProvider.authenticateUser(userId,edit,password) ) { authenticatedProvider.set(providerID); return true; } else if ( nextProvider != null ) { return nextProvider.authenticateUser(userId,edit,password); } return false; } // authenticateUser /** * The UserDirectoryProvider used by this filter * @return */ public UserDirectoryProvider getMyProvider() { return myProvider; } /** * The UserDirectoryProvider used by this filter * @param myProvider */ public void setMyProvider(UserDirectoryProvider myProvider) { this.myProvider = myProvider; } /** * The Next Directory Provider in the chain * @return */ public UserDirectoryProvider getNextProvider() { return nextProvider; } /** * The Next Directory Provider in the chain * @param nextDirectoryProvider */ public void setNextProvider(UserDirectoryProvider nextDirectoryProvider) { this.nextProvider = nextDirectoryProvider; } /** * {@inheritDoc} */ public boolean authenticateWithProviderFirst(String id) { return false; } /** * {@inheritDoc} */ public List<UserEdit> searchExternalUsers(String criteria, int first, int last, UserFactory factory) { List<UserEdit> users = new ArrayList<UserEdit>(); if ( myProvider instanceof ExternalUserSearchUDP ) { ExternalUserSearchUDP extSearchUDP = (ExternalUserSearchUDP) myProvider; if (m_logger.isDebugEnabled()) { m_logger.debug("searchExternalUsers() criteria=" + criteria); } users.addAll(extSearchUDP.searchExternalUsers(criteria, first, last, factory)); } if ( nextProvider instanceof ExternalUserSearchUDP) { ExternalUserSearchUDP extSearchUDP = (ExternalUserSearchUDP) nextProvider; if (m_logger.isDebugEnabled()) { m_logger.debug("nextProvider searchExternalUsers() criteria=" + criteria); } users.addAll(extSearchUDP.searchExternalUsers(criteria, first, last, factory)); } return users; } } // FilterUserDirectoryProvider