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