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