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