001/*
002 *  Copyright 2016 Anyware Services
003 *
004 *  Licensed under the Apache License, Version 2.0 (the "License");
005 *  you may not use this file except in compliance with the License.
006 *  You may obtain a copy of the License at
007 *
008 *      http://www.apache.org/licenses/LICENSE-2.0
009 *
010 *  Unless required by applicable law or agreed to in writing, software
011 *  distributed under the License is distributed on an "AS IS" BASIS,
012 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 *  See the License for the specific language governing permissions and
014 *  limitations under the License.
015 */
016package org.ametys.core.user;
017
018import java.util.ArrayList;
019import java.util.Collection;
020import java.util.Collections;
021import java.util.Comparator;
022import java.util.List;
023import java.util.Map;
024import java.util.Set;
025import java.util.stream.Collectors;
026
027import org.apache.avalon.framework.activity.Initializable;
028import org.apache.avalon.framework.component.Component;
029import org.apache.avalon.framework.service.ServiceException;
030import org.apache.avalon.framework.service.ServiceManager;
031import org.apache.avalon.framework.service.Serviceable;
032import org.apache.commons.lang3.tuple.Pair;
033
034import org.ametys.core.cache.AbstractCacheManager;
035import org.ametys.core.cache.Cache;
036import org.ametys.core.cache.CacheException;
037import org.ametys.core.user.directory.NotUniqueUserException;
038import org.ametys.core.user.directory.StoredUser;
039import org.ametys.core.user.directory.UserDirectory;
040import org.ametys.core.user.population.PopulationContextHelper;
041import org.ametys.core.user.population.UserPopulation;
042import org.ametys.core.user.population.UserPopulationDAO;
043import org.ametys.core.util.LambdaUtils;
044import org.ametys.core.util.LambdaUtils.LambdaException;
045import org.ametys.plugins.core.user.UserHelper;
046import org.ametys.runtime.i18n.I18nizableText;
047import org.ametys.runtime.plugin.component.AbstractLogEnabled;
048
049/**
050 * Component for getting user list and verify the presence of a particular user on a context or for user directory(ies).
051 */
052public class UserManager extends AbstractLogEnabled implements Component, Serviceable, Initializable
053{
054    /** Avalon Role */
055    public static final String ROLE = UserManager.class.getName();
056
057    private static final String __USER_CACHE_ID = UserHelper.class.getName() + "$userCache";
058    
059    private static final String __USER_CACHE_BY_EMAIL_ID = UserHelper.class.getName() + "$userByEmailCache";
060    
061    /** The DAO for User Population */
062    protected UserPopulationDAO _userPopulationDAO;
063    /** The helper for the associations population/context */
064    protected PopulationContextHelper _populationContextHelper;
065
066    private AbstractCacheManager _abstractCacheManager;
067    /** The user factory */
068    private UserFactory _userFactory;
069    
070    @Override
071    public void service(ServiceManager manager) throws ServiceException
072    {
073        _userPopulationDAO = (UserPopulationDAO) manager.lookup(UserPopulationDAO.ROLE);
074        _populationContextHelper = (PopulationContextHelper) manager.lookup(PopulationContextHelper.ROLE);
075        _abstractCacheManager = (AbstractCacheManager) manager.lookup(AbstractCacheManager.ROLE);
076        _userFactory = (UserFactory) manager.lookup(UserFactory.ROLE);
077    }
078
079    @Override
080    public void initialize() throws Exception
081    {
082        _abstractCacheManager.createRequestCache(__USER_CACHE_ID,
083                new I18nizableText("plugin.core", "PLUGINS_CORE_CACHE_USER_BY_USER_IDENTITY_LABEL"),
084                new I18nizableText("plugin.core", "PLUGINS_CORE_CACHE_USER_BY_USER_IDENTITY_DESCRIPTION"),
085                true);
086        _abstractCacheManager.createRequestCache(__USER_CACHE_BY_EMAIL_ID,
087                new I18nizableText("plugin.core", "PLUGINS_CORE_CACHE_USER_BY_EMAIL_LABEL"),
088                new I18nizableText("plugin.core", "PLUGINS_CORE_CACHE_USER_BY_EMAIL_DESCRIPTION"),
089                true);
090    }
091    
092    /**
093     * Get the list of users on some given contexts
094     * @param contexts The contexts
095     * @param checkRight True to check that current user belongs to one of populations on theses contexts or he's an administrator user
096     * @return the collection of users
097     */
098    public Collection<User> getUsersByContext(Set<String> contexts, boolean checkRight)
099    {
100        List<UserPopulation> userPopulations = _populationContextHelper.getUserPopulationsOnContexts(contexts, false, checkRight).stream()
101            .map(upId -> _userPopulationDAO.getUserPopulation(upId))
102            .collect(Collectors.toList());
103        
104        return getUsersByPopulations(userPopulations);
105    }
106    
107    /**
108     * Get the users for given users' populations
109     * @param userPopulationIds the id of population of users
110     * @return the collection of users
111     */
112    public Collection<User> getUsersByPopulationIds(List<String> userPopulationIds)
113    {
114        List<User> users = new ArrayList<>();
115        for (String id : userPopulationIds)
116        {
117            UserPopulation userPopulation = _userPopulationDAO.getUserPopulation(id);
118            if (userPopulation != null)
119            {
120                for (User user : getUsers(userPopulation))
121                {
122                    if (!users.contains(user))
123                    {
124                        users.add(user);
125                    }
126                }
127            }
128        }
129        return users;
130    }
131    
132    /**
133     * Get the users for given users' populations
134     * @param userPopulations the population of users
135     * @return the collection of users
136     */
137    public Collection<User> getUsersByPopulations(List<UserPopulation> userPopulations)
138    {
139        List<User> users = new ArrayList<>();
140        for (UserPopulation userPopulation : userPopulations)
141        {
142            for (User user : getUsers(userPopulation))
143            {
144                if (!users.contains(user))
145                {
146                    users.add(user);
147                }
148            }
149        }
150        return users;
151    }
152    
153    /**
154     * Gets all the users of a {@link UserPopulation}
155     * @param userPopulationId The ID of user population
156     * @return list of users as Collection of {@link User}s, empty if a problem occurs.
157     */
158    public Collection<User> getUsers(String userPopulationId)
159    {
160        UserPopulation userPopulation = _userPopulationDAO.getUserPopulation(userPopulationId);
161        if (userPopulation != null)
162        {
163            return getUsers(userPopulation);
164        }
165        else
166        {
167            return Collections.EMPTY_LIST;
168        }
169    }
170    
171    /**
172     * Gets all the users of a {@link UserPopulation}
173     * @param userPopulation The user population
174     * @return list of users as Collection of {@link User}s, empty if a problem occurs.
175     */
176    public Collection<User> getUsers(UserPopulation userPopulation)
177    {
178        List<User> users = new ArrayList<>();
179        
180        for (UserDirectory ud : userPopulation.getUserDirectories())
181        {
182            Collection<User> usersOfUd = _storedUsers2Users(ud.getStoredUsers(), ud);
183            for (User user : usersOfUd)
184            {
185                if (!users.contains(user))
186                {
187                    users.add(user);
188                }
189            }
190        }
191        
192        return users;
193    }
194    
195    /**
196     * Get a list of users given the parameters
197     * @param contexts The contexts
198     * @param count The limit of users to retrieve
199     * @param offset The number of result to ignore before starting to collect users.
200     * @param parameters A map of additional parameters, see implementation.
201     * @param checkRight True to check that current user belongs to one of populations on theses contexts or he's an administrator user
202     * @param sort true to sort users
203     * @return The list of retrieved {@link User}
204     */
205    public List<User> getUsersByContext(Set<String> contexts, int count, int offset, Map<String, Object> parameters, boolean checkRight, boolean sort)
206    {
207        List<UserPopulation> userPopulations = _populationContextHelper.getUserPopulationsOnContexts(contexts, false, checkRight).stream()
208                .map(upId -> _userPopulationDAO.getUserPopulation(upId))
209                .collect(Collectors.toList());
210        
211        return getUsers(userPopulations, count, offset, parameters, sort);
212    }
213    
214    /**
215     * Get a list of users given the parameters
216     * @param userPopulations the population of users
217     * @param count The limit of users to retrieve
218     * @param offset The number of result to ignore before starting to collect users.
219     * @param parameters A map of additional parameters, see implementation.
220     * @param sort true to sort users
221     * @return The list of retrieved {@link User}
222     */
223    public List<User> getUsers(List<UserPopulation> userPopulations, int count, int offset, Map<String, Object> parameters, boolean sort)
224    {
225        List<User> users = new ArrayList<>();
226        for (UserPopulation userPopulation : userPopulations)
227        {
228            for (User user : getUsers(userPopulation, count + offset, 0, parameters, sort))
229            {
230                if (!users.contains(user))
231                {
232                    users.add(user);
233                }
234            }
235        }
236        
237        int boundedCount = count >= 0 ? count : Integer.MAX_VALUE;
238        int boundedOffset = offset >= 0 ? offset : 0;
239        int toIndex;
240        if (boundedOffset + boundedCount >= 0)
241        {
242            toIndex = Math.min(boundedOffset + boundedCount, users.size());
243        }
244        else
245        {
246            // particular case where count was initially negative (to say "no limit") and we set it to Integer.MAX_VALUE
247            // so if the offset is strictly positive, the sum overflows
248            toIndex = users.size();
249        }
250        return users.subList(boundedOffset, toIndex);
251    }
252    
253    /**
254     * Gets all the users of a {@link UserPopulation}
255     * @param userPopulationId The ID of user population
256     * @param count The limit of users to retrieve
257     * @param offset The number of result to ignore before starting to collect users.
258     * @param parameters A map of additional parameters, see implementation.
259     * @param sort true to sort users
260     * @return list of users as Collection of {@link User}s, empty if a problem occurs.
261     */
262    public Collection<User> getUsers(String userPopulationId, int count, int offset, Map<String, Object> parameters, boolean sort)
263    {
264        UserPopulation userPopulation = _userPopulationDAO.getUserPopulation(userPopulationId);
265        if (userPopulation != null)
266        {
267            return getUsers(userPopulation, count, offset, parameters, sort);
268        }
269        else
270        {
271            return Collections.EMPTY_LIST;
272        }
273    }
274    
275    /**
276     * Gets all the users of a given {@link UserPopulation} and {@link UserDirectory}
277     * @param userPopulationId The ID of user population
278     * @param userDirectoryId The id of the user directory
279     * @param count The limit of users to retrieve
280     * @param offset The number of result to ignore before starting to collect users.
281     * @param parameters A map of additional parameters, see implementation.
282     * @return list of users as Collection of {@link User}s, empty if a problem occurs.
283     */
284    public Collection<User> getUsersByDirectory(String userPopulationId, String userDirectoryId, int count, int offset, Map<String, Object> parameters)
285    {
286        UserPopulation userPopulation = _userPopulationDAO.getUserPopulation(userPopulationId);
287        
288        if (userPopulation == null)
289        {
290            return Collections.EMPTY_LIST;
291        }
292        
293        UserDirectory ud = userPopulation.getUserDirectory(userDirectoryId);
294        if (ud == null)
295        {
296            throw new IllegalArgumentException("In the population '" + userPopulationId + "' the directory '" + userDirectoryId + "' was referenced but does not exists");
297        }
298        return _storedUsers2Users(ud.getStoredUsers(count, offset, parameters), ud);
299    }
300    
301    /**
302     * Gets all the users of a {@link UserPopulation}
303     * @param userPopulation The users population
304     * @param count The limit of users to retrieve
305     * @param offset The number of result to ignore before starting to collect users.
306     * @param parameters A map of additional parameters, see implementation.
307     * @param sort true to sort users
308     * @return list of users as Collection of {@link User}s, empty if a problem occurs.
309     */
310    public Collection<User> getUsers(UserPopulation userPopulation, int count, int offset, Map<String, Object> parameters, boolean sort)
311    {
312        List<User> users = new ArrayList<>();
313        
314        for (UserDirectory ud : userPopulation.getUserDirectories())
315        {
316            Collection<User> usersOfUd = _storedUsers2Users(ud.getStoredUsers(-1, 0, parameters), ud);
317            for (User user : usersOfUd)
318            {
319                if (!users.contains(user))
320                {
321                    users.add(user);
322                }
323            }
324        }
325        
326        int boundedCount = count >= 0 ? count : Integer.MAX_VALUE;
327        int boundedOffset = offset >= 0 ? offset : 0;
328        int toIndex;
329        if (boundedOffset + boundedCount >= 0)
330        {
331            toIndex = Math.min(boundedOffset + boundedCount, users.size());
332        }
333        else
334        {
335            // particular case where count was initially negative (to say "no limit") and we set it to Integer.MAX_VALUE
336            // so if the offset is strictly positive, the sum overflows
337            toIndex = users.size();
338        }
339        
340        // Use isTrue as sort parameter can be null
341        if (sort)
342        {
343            users.sort(Comparator.comparing(UserManager::_getSortableNameLowerCase));
344        }
345        
346        return users.subList(boundedOffset, toIndex);
347    }
348    
349    private static String _getSortableNameLowerCase(User user)
350    {
351        return user.getSortableName().toLowerCase();
352    }
353    
354    /**
355     * Gets all the users of a {@link UserPopulation}
356     * @param userPopulation The users population
357     * @param userDirectoryId The id of the user directory
358     * @param count The limit of users to retrieve
359     * @param offset The number of result to ignore before starting to collect users.
360     * @param parameters A map of additional parameters, see implementation.
361     * @return list of users as Collection of {@link User}s, empty if a problem occurs.
362     */
363    public Collection<User> getUsersByDirectory(UserPopulation userPopulation, String userDirectoryId, int count, int offset, Map<String, Object> parameters)
364    {
365        UserDirectory ud = userPopulation.getUserDirectory(userDirectoryId);
366        return _storedUsers2Users(ud.getStoredUsers(count, offset, parameters), ud);
367    }
368    
369    /**
370     * Get a user by his login on some given contexts
371     * @param contexts The contexts
372     * @param login Login of the user to get. Cannot be null.
373     * @param checkRight True to check that current user is authorized to retrieve this user (true if he belongs to one of populations on theses contexts or he's an administrator user)
374     * @return User's information as a {@link User} instance or null if the user login does not exist.
375     */
376    public User getUserByContext(Set<String> contexts, String login, boolean checkRight)
377    {
378        Set<String> upIds = _populationContextHelper.getUserPopulationsOnContexts(contexts, false, checkRight);
379        for (String upId : upIds)
380        {
381            UserPopulation userPopulation = _userPopulationDAO.getUserPopulation(upId);
382            User user = getUser(userPopulation, login);
383            if (user != null)
384            {
385                return user;
386            }
387        }
388        return null;
389    }
390    
391    /**
392     * Get the user from its user identity
393     * @param userIdentity The user identity
394     * @return The User or null if the user login does not exist.
395     */
396    public User getUser (UserIdentity userIdentity)
397    {
398        if (userIdentity == null)
399        {
400            return null;
401        }
402        
403        Cache<UserIdentity, User> cache = _getUserCache();
404        return cache.get(userIdentity, key ->
405        {
406            UserPopulation userPopulation = _userPopulationDAO.getUserPopulation(key.getPopulationId());
407            if (userPopulation != null)
408            {
409                return getUser(userPopulation, key.getLogin());
410            }
411            else
412            {
413                return null;
414            }
415        });
416    }
417    
418    /**
419     * Get a particular user of the given users population by his login.
420     * @param userPopulationId The ID of user population
421     * @param login Login of the user to get. Cannot be null.
422     * @return User's information as a {@link User} instance or null if the user login does not exist.
423     */
424    public User getUser(String userPopulationId, String login)
425    {
426        UserIdentity userIdentity = new UserIdentity(login, userPopulationId);
427        return getUser(userIdentity);
428    }
429
430    private Cache<UserIdentity, User> _getUserCache()
431    {
432        return _abstractCacheManager.get(__USER_CACHE_ID);
433    }
434    
435    /**
436     * Get a particular user of the given users population by his email.
437     * @param userPopulationIds The IDs of user population
438     * @param email Email of the user to get. Cannot be null.
439     * @return User's information as a {@link User} instance or null if the user login does not exist.
440     * @throws NotUniqueUserException if many users match the given email
441     */
442    public User getUserByEmail(Set<String> userPopulationIds, String email) throws NotUniqueUserException
443    {
444        if (email == null)
445        {
446            return null;
447        }
448        
449        User user = null;
450        for (String populationId : userPopulationIds)
451        {
452            User u = getUserByEmail(populationId, email);
453            if (u != null)
454            {
455                if (user == null)
456                {
457                    user = u;
458                }
459                else
460                {
461                    throw new NotUniqueUserException("Cannot find user in populations '" + userPopulationIds + "' by email '" + email + "' because 2 or more users match (at least " + UserIdentity.userIdentityToString(u.getIdentity()) + " and " + UserIdentity.userIdentityToString(user.getIdentity()) + ")");
462                }
463            }
464        }
465        
466        return user;
467    }
468    
469    /**
470     * Get a particular user of the given users population by his email.
471     * @param userPopulationId The ID of user population
472     * @param email Email of the user to get. Cannot be null.
473     * @return User's information as a {@link User} instance or null if the user login does not exist.
474     * @throws NotUniqueUserException if many users match the given email
475     */
476    public User getUserByEmail(String userPopulationId, String email) throws NotUniqueUserException
477    {
478        if (email == null)
479        {
480            return null;
481        }
482
483        Cache<Pair<String, String>, User> cache = _getUserByEmailCache();
484        try
485        {
486            return cache.get(Pair.of(userPopulationId, email), LambdaUtils.wrap(key ->
487            {
488                UserPopulation userPopulation = _userPopulationDAO.getUserPopulation(userPopulationId);
489                if (userPopulation != null)
490                {
491                    return getUserByEmail(userPopulation, email);
492                }
493                else
494                {
495                    return null;
496                }
497            }));
498        }
499        catch (CacheException e)
500        {
501            if (e.getCause() instanceof LambdaException)
502            {
503                if (e.getCause().getCause() instanceof NotUniqueUserException)
504                {
505                    throw (NotUniqueUserException) e.getCause().getCause();
506                }
507                else
508                {
509                    throw new RuntimeException(e.getCause().getCause());
510                }
511            }
512            throw new RuntimeException(e.getCause());
513        }
514    }
515
516    private Cache<Pair<String, String>, User> _getUserByEmailCache()
517    {
518        return _abstractCacheManager.get(__USER_CACHE_BY_EMAIL_ID);
519    }
520    
521    /**
522     * Get a particular user of the given user population and given user directory by his login.
523     * @param userPopulationId The ID of user population
524     * @param userDirectoryId The id of the user directory
525     * @param login Login of the user to get. Cannot be null.
526     * @return User's information as a {@link User} instance or null if the user login does not exist.
527     */
528    public User getUserByDirectory(String userPopulationId, String userDirectoryId, String login)
529    {
530        UserPopulation userPopulation = _userPopulationDAO.getUserPopulation(userPopulationId);
531        if (userPopulation != null)
532        {
533            return getUserByDirectory(userPopulation, userDirectoryId, login);
534        }
535        else
536        {
537            return null;
538        }
539    }
540    
541    /**
542     * Get a particular user of the given user population by his login.
543     * @param userPopulation The user population
544     * @param login Login of the user to get. Cannot be null.
545     * @return User's information as a {@link User} instance or null if the user login does not exist.
546     */
547    public User getUser(UserPopulation userPopulation, String login)
548    {
549        for (UserDirectory ud : userPopulation.getUserDirectories())
550        {
551            StoredUser storedUser = ud.getStoredUser(login);
552            if (storedUser != null)
553            {
554                return _storedUser2User(storedUser, ud);
555            }
556        }
557        return null;
558    }
559    
560    /**
561     * Get a particular user of the given user population by his email.
562     * @param userPopulation The user population
563     * @param email Email of the user to get. Cannot be null.
564     * @return User's information as a {@link User} instance or null if the user login does not exist.
565     * @throws NotUniqueUserException if many users match the given email
566     */
567    public User getUserByEmail(UserPopulation userPopulation, String email) throws NotUniqueUserException
568    {
569        for (UserDirectory ud : userPopulation.getUserDirectories())
570        {
571            StoredUser storedUser = ud.getStoredUserByEmail(email);
572            if (storedUser != null)
573            {
574                return _storedUser2User(storedUser, ud);
575            }
576        }
577        return null;
578    }
579
580    /**
581     * Get a particular user of the given user directory by his email.
582     * @param userDirectory The user directory
583     * @param email Email of the user to get. Cannot be null.
584     * @return User's information as a {@link User} instance or null if the user login does not exist.
585     * @throws NotUniqueUserException if many users match the given email
586     */
587    public User getUserByUserDirectoryAndEmail(UserDirectory userDirectory, String email) throws NotUniqueUserException
588    {
589        StoredUser storedUser = userDirectory.getStoredUserByEmail(email);
590        if (storedUser != null)
591        {
592            return _storedUser2User(storedUser, userDirectory);
593        }
594        
595        return null;
596    }
597    
598    /**
599     * Get a particular user of the given user population and given user directory by his login.
600     * @param userPopulation The user population
601     * @param userDirectoryId The id of the user directory
602     * @param login Login of the user to get. Cannot be null.
603     * @return User's information as a {@link User} instance or null if the user login does not exist.
604     */
605    public User getUserByDirectory(UserPopulation userPopulation, String userDirectoryId, String login)
606    {
607        UserDirectory ud = userPopulation.getUserDirectory(userDirectoryId);
608        
609        StoredUser storedUser = ud.getStoredUser(login);
610        if (storedUser != null)
611        {
612            return _storedUser2User(storedUser, ud);
613        }
614        
615        return null;
616    }
617    
618    /**
619     * Get a particular user of the given user directory by his login.
620     * @param userDirectory The user directory
621     * @param login Login of the user to get. Cannot be null.
622     * @return User's information as a {@link User} instance or null if the user login does not exist.
623     */
624    public User getUserByDirectory(UserDirectory userDirectory, String login)
625    {
626        StoredUser storedUser = userDirectory.getStoredUser(login);
627        if (storedUser != null)
628        {
629            return _storedUser2User(storedUser, userDirectory);
630        }
631        
632        return null;
633    }
634    
635    /**
636     * Get the user directory the given user belongs to
637     * @param userPopulationId The id of the user population
638     * @param login Login of the user to get. Cannot be null.
639     * @return The user directory the user belongs to.
640     */
641    public UserDirectory getUserDirectory(String userPopulationId, String login)
642    {
643        UserPopulation userPopulation = _userPopulationDAO.getUserPopulation(userPopulationId);
644        if (userPopulation == null)
645        {
646            return null;
647        }
648        
649        for (UserDirectory ud : userPopulation.getUserDirectories())
650        {
651            StoredUser storedUser = ud.getStoredUser(login);
652            if (storedUser != null)
653            {
654                return ud;
655            }
656        }
657        return null;
658    }
659    
660    /**
661     * Convert a {@link StoredUser} to a {@link User}
662     * @param storedUser The stored user to convert
663     * @param userDirectory the user directory containing the StoredUser
664     * @return A {@link User}
665     */
666    private User _storedUser2User(StoredUser storedUser, UserDirectory userDirectory)
667    {
668        return _userFactory.createUser(storedUser, userDirectory);
669    }
670    
671    /**
672     * Convert a Collection of {@link StoredUser} from the same user directory to a Collection of {@link User}
673     * @param storedUsers The collection of stored user to convert
674     * @param userDirectory the user directory containing the stored users
675     * @return A {@link User}
676     */
677    private Collection<User> _storedUsers2Users(Collection<StoredUser> storedUsers, UserDirectory userDirectory)
678    {
679        Collection<User> users = new ArrayList<>();
680        for (StoredUser storedUser : storedUsers)
681        {
682            users.add(_storedUser2User(storedUser, userDirectory));
683        }
684        
685        return users;
686    }
687}