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