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