001/*
002 *  Copyright 2012 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.io.Closeable;
019import java.io.IOException;
020import java.io.InputStream;
021import java.time.ZonedDateTime;
022import java.util.List;
023import java.util.TimeZone;
024import java.util.stream.Collectors;
025import java.util.stream.Stream;
026
027import org.apache.commons.lang3.StringUtils;
028import org.slf4j.Logger;
029
030import org.ametys.core.user.dataprovider.UserDataProvider;
031import org.ametys.core.user.directory.StoredUser;
032import org.ametys.core.user.directory.UserDirectory;
033import org.ametys.core.user.status.UserStatusManager;
034import org.ametys.core.util.SizeUtils.ExcludeFromSizeCalculation;
035
036/**
037 * Implementation of the principal abstraction to represent an user with a login and a {@link StoredUser}
038 * @param <T> The type of the factory
039 */
040public class User <T extends UserFactory> implements java.security.Principal
041{
042    /** The id of the last name data. This data is of type {@link String} */
043    public static final String LAST_NAME_DATA_ID = "lastname";
044    /** The id of the first name data. This data is of type {@link String} */
045    public static final String FIRST_NAME_DATA_ID = "firstname";
046    /** The id of the email data. This data is of type {@link String} */
047    public static final String EMAIL_DATA_ID = "email";
048    /** The id of the image data. This data is of type {@link UserImage} */
049    public static final String IMAGE_DATA_ID = "image";
050    /** The id of the language data. This data is of type String */
051    public static final String LANGUAGE_DATA_ID = "language";
052    /** The id of the timezone data. This data is of type {@link TimeZone} or is a TimeZoneId of type {@link String} */
053    public static final String TIMEZONE_DATA_ID = "timezone";
054    /** The id of the creation date data. This data is of type {@link ZonedDateTime} */
055    public static final String CREATION_DATE_DATA_ID = "creation-date";
056    /** The id of the creation origin data. This data is of type {@link UserCreationOrigin} */
057    public static final String CREATION_ORIGIN_DATA_ID = "creation-origin";
058
059    /**
060     * The identity of this principal.
061     */
062    protected UserIdentity _identity;
063    /**
064     * The user directory this user belongs to.
065     */
066    @ExcludeFromSizeCalculation
067    protected UserDirectory _userDirectory;
068    
069    /** The factory */
070    @ExcludeFromSizeCalculation
071    protected T _factory;
072    
073    @ExcludeFromSizeCalculation
074    private Logger _logger;
075
076    private StoredUser _storedUser;
077    
078    /**
079     * Enumeration for the user creation origin
080     *
081     */
082    public enum UserCreationOrigin
083    {
084        /** User created by system */
085        SYSTEM,
086        /** User created by an administrator */
087        ADMIN,
088        /** User created by user signup */
089        USER_SIGNUP,
090        /** When user creation is unknown or not available */
091        NOT_AVAILABLE
092    }
093    
094    /**
095     * Basic structure holding necessary data representing an user profile image
096     */
097    public static class UserImage implements Closeable
098    {
099        private final InputStream _inputstream;
100        private final String _filename;
101        private final Long _length;
102        private final Long _lastModified;
103        private final String _type;
104        
105        /**
106         * Constructor
107         * @param inputstream The image input stream
108         * @param filename The file name or null if unknown
109         * @param length The file length if known
110         * @param lastModified The last modified date
111         * @param type The type name of the source of the image to compute a cache key
112         */
113        public UserImage(InputStream inputstream, String filename, Long length, Long lastModified, String type)
114        {
115            _inputstream = inputstream;
116            _filename = filename;
117            _length = length;
118            _lastModified = lastModified;
119            _type = type;
120        }
121        
122        /**
123         * Retrieve the type name of the source of the image to compute a cache key
124         * @return the type
125         */
126        public String getType()
127        {
128            return _type;
129        }
130        
131        /**
132         * Retrieves the input stream
133         * @return the input stream
134         */
135        public InputStream getInputstream()
136        {
137            return _inputstream;
138        }
139
140        /**
141         * Retrieves the filename
142         * @return the filename or null if not defined
143         */
144        public String getFilename()
145        {
146            return _filename;
147        }
148
149        /**
150         * Retrieves the length
151         * @return the length or null or -1 if unknown
152         */
153        public Long getLength()
154        {
155            return _length;
156        }
157        
158        /**
159         * Retrieve the last modified date.
160         * @return The last modified date. Can be null if unknown.
161         */
162        public Long getLastModified()
163        {
164            return _lastModified;
165        }
166        
167        /**
168         * Close input stream
169         * @implNote Close the input stream of the image.
170         */
171        public void close() throws IOException
172        {
173            _inputstream.close();
174        }
175    }
176    
177    /**
178     * Construct a new UserPrincipal, associated with a last name, a first name,
179     * an email and identified by a login.
180     * @param storedUser The storedUser. Cannot be null
181     * @param userDirectory The user directory the user belongs to. Cannot be null.
182     * @param factory The factory
183     */
184    User(StoredUser storedUser, UserDirectory userDirectory, T factory, Logger logger)
185    {
186        _identity = new UserIdentity(storedUser.getIdentifier(), userDirectory.getPopulationId());
187        _storedUser = storedUser;
188        _userDirectory = userDirectory;
189        _factory = factory;
190        _logger = logger;
191    }
192
193    /**
194     * The identity of the user.
195     * 
196     * @return The identity.
197     */
198    public UserIdentity getIdentity()
199    {
200        return _identity;
201    }
202    
203    /**
204     * Get the StoredUser of the User
205     * @return The {@link StoredUser}
206     */
207    public StoredUser getStoredUser()
208    {
209        return _storedUser;
210    }
211    
212    @Override
213    public String getName()
214    {
215        return UserIdentity.userIdentityToString(_identity);
216    }
217    
218    /**
219     * The user directory this user belongs to.
220     * @return The user directory
221     */
222    public UserDirectory getUserDirectory()
223    {
224        return _userDirectory;
225    }
226    
227    /**
228     * The last name of the user
229     * @return The last name.
230     */
231    public String getLastName()
232    {
233        return (String) getValue(LAST_NAME_DATA_ID);
234    }
235    
236    /**
237     * The first name of the user
238     * @return The first name.
239     */
240    public String getFirstName()
241    {
242        return (String) getValue(FIRST_NAME_DATA_ID);
243    }
244    
245    /**
246     * The email of the user represented by this Principal.
247     * 
248     * @return The email.
249     */
250    public String getEmail()
251    {
252        return (String) getValue(EMAIL_DATA_ID);
253    }
254    
255    /**
256     * The fullname of this user.
257     * @return The full name
258     */
259    public String getFullName()
260    {
261        return _getFullName(true);
262    }
263    
264    /**
265     * The fullname to use to display if sort is needed.
266     * Ensure the sort will be on
267     * @return The sortable name
268     */
269    public String getSortableName()
270    {
271        return _getFullName(false);
272    }
273    
274    /**
275     * Get user's avatar from the {@link UserDataProvider} with the biggest priority
276     * @param size The size
277     * @param maxSize The maxSize
278     * @return A {@link UserImage} the user's avatar or default image if an error occurs
279     */
280    public UserImage getImage(int size, int maxSize)
281    {
282        UserImageAccessor imageAccessor = (UserImageAccessor) getValue(IMAGE_DATA_ID);
283        
284        try
285        {
286            return imageAccessor != null ? imageAccessor.getImage(size, maxSize) : null;
287        }
288        catch (Exception e)
289        {
290            _logger.error("An error occurred while retrieving the profile image for user '" + getIdentity() + "'. Returning default image.", e);
291            return _factory.getDefaultUserImageHelper().getDefaultImage();
292        }
293    }
294    
295    /**
296     * Get user's language from the {@link UserDataProvider} with the biggest priority
297     * @return The user's language
298     */
299    public String getLanguage()
300    {
301        return (String) getValue(LANGUAGE_DATA_ID);
302    }
303    
304    /**
305     * Get user's TimeZone from the {@link UserDataProvider} with the biggest priority
306     * @return The user's timezone
307     */
308    public TimeZone getTimeZone()
309    {
310        Object timezoneFound = getValue(TIMEZONE_DATA_ID);
311        if (timezoneFound instanceof TimeZone timeZone)
312        {
313            return timeZone;
314        }
315        else if (timezoneFound instanceof String timeZoneId && List.of(TimeZone.getAvailableIDs()).contains(timeZoneId))
316        {
317            return TimeZone.getTimeZone(timeZoneId);
318        }
319        
320        return null;
321    }
322    
323    /**
324     * Get the last connection date of the user
325     * @implNote in safe mode, the information is not available and this method always return null
326     * @return the last connection date or null if the user never logged in
327     */
328    public ZonedDateTime getLastConnectionDate()
329    {
330        UserStatusManager userStatusManager = _factory.getUserStatusManager();
331        if (userStatusManager != null)
332        {
333            return userStatusManager.getLastConnectionDate(this.getIdentity()).orElse(null);
334        }
335        return null;
336    }
337    
338    /**
339     * Get the user's creation date
340     * @return the creation date
341     */
342    public ZonedDateTime getCreationDate()
343    {
344        return (ZonedDateTime) getValue(CREATION_DATE_DATA_ID);
345    }
346    
347    /**
348     * Get the user's creation origin
349     * @return the creation origin
350     */
351    public UserCreationOrigin getCreationOrigin()
352    {
353        return (UserCreationOrigin) getValue(CREATION_ORIGIN_DATA_ID);
354    }
355    
356    /**
357     * Get a user's data from the {@link UserDataProvider} with the biggest priority
358     * @param dataId The data id
359     * @return A user's data
360     */
361    public Object getValue(String dataId)
362    {
363        return _factory.getUserDataProviderEP().getValue(this, dataId);
364    }
365    
366    /**
367     * Get the {@link UserDataProvider} with the biggest priority and a value not null for the data requested
368     * @param dataId The data id
369     * @return A {@link UserDataProvider} the user data provider which is going to provide the data requested
370     */
371    public UserDataProvider getProviderFor(String dataId)
372    {
373        return _factory.getUserDataProviderEP().getProviderFor(this, dataId);
374    }
375    
376    /**
377     * The full name of the user represented by this Principal.
378     * @param firstnameThenLastname Define the name order in the full name. Set to <code>true</code> to retrieve the fullname with firstname first and lastname second. Set to <code>false</code> to retrieve the fullname with lastname first and firstname second.
379     * @return The full name.
380     */
381    protected String _getFullName(boolean firstnameThenLastname)
382    {
383        Stream<String> stream = firstnameThenLastname
384                ? Stream.of(getFirstName(), getLastName())
385                : Stream.of(getLastName(), getFirstName());
386        
387        String sortableName = stream.filter(StringUtils::isNotEmpty)
388            .collect(Collectors.joining(" "));
389        
390        return StringUtils.defaultIfEmpty(sortableName, _identity.getLogin());
391    }
392    
393    /**
394     * Return a String representation of this object, which exposes only
395     * information that should be public.
396     * 
397     * @return A string representing the user.
398     */
399    @Override
400    public String toString()
401    {
402        StringBuilder sb = new StringBuilder("Principal[");
403        sb.append(_userDirectory.getPopulationId());
404        sb.append(", ");
405        sb.append(_storedUser.toString());
406        sb.append("]");
407        return sb.toString();
408    }
409
410    /**
411     * Test if two principal are equals.
412     * @return true if the given Object represents the same Principal.
413     */
414    @Override
415    public boolean equals(Object another)
416    {
417        if (another == null || !(another instanceof User))
418        {
419            return false;
420        }
421        
422        User otherUser = (User) another;
423        
424        return _identity != null  && _identity.equals(otherUser.getIdentity());
425    }
426    
427    @Override
428    public int hashCode()
429    {
430        return _identity.hashCode();
431    }
432}