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.plugins.core.ui.user;
017
018import java.awt.Graphics;
019import java.awt.image.BufferedImage;
020import java.io.ByteArrayInputStream;
021import java.io.ByteArrayOutputStream;
022import java.io.IOException;
023import java.io.InputStream;
024import java.util.Collections;
025import java.util.HashMap;
026import java.util.LinkedHashMap;
027import java.util.LinkedList;
028import java.util.List;
029import java.util.Map;
030import java.util.NoSuchElementException;
031
032import javax.imageio.ImageIO;
033
034import org.apache.avalon.framework.component.Component;
035import org.apache.avalon.framework.configuration.Configuration;
036import org.apache.avalon.framework.configuration.ConfigurationException;
037import org.apache.avalon.framework.configuration.DefaultConfigurationBuilder;
038import org.apache.avalon.framework.context.Context;
039import org.apache.avalon.framework.context.ContextException;
040import org.apache.avalon.framework.context.Contextualizable;
041import org.apache.avalon.framework.service.ServiceException;
042import org.apache.avalon.framework.service.ServiceManager;
043import org.apache.cocoon.ProcessingException;
044import org.apache.cocoon.ResourceNotFoundException;
045import org.apache.cocoon.components.ContextHelper;
046import org.apache.cocoon.environment.Request;
047import org.apache.cocoon.util.HashUtil;
048import org.apache.commons.codec.binary.Base64;
049import org.apache.commons.io.FilenameUtils;
050import org.apache.commons.lang3.StringUtils;
051import org.apache.excalibur.source.Source;
052import org.xml.sax.SAXException;
053
054import org.ametys.core.upload.Upload;
055import org.ametys.core.upload.UploadManager;
056import org.ametys.core.user.User;
057import org.ametys.core.user.UserIdentity;
058import org.ametys.core.userpref.UserPreferencesManager;
059import org.ametys.core.util.ImageHelper;
060import org.ametys.core.util.JSONUtils;
061
062/**
063 * Helper providing images that are used for user profiles
064 */
065public class DefaultProfileImageProvider extends SafeProfileImageProvider implements Contextualizable, Component
066{
067    /**
068     * Profile image source enum
069     */
070    public enum ProfileImageSource
071    {
072        /** Local images */
073        LOCALIMAGE,
074        /** Gravatar */
075        GRAVATAR,
076        /** Provided by the users manager */
077        USERSMANAGER,
078        /** Image with the initial */
079        INITIALS,
080        /** Uploaded image */
081        UPLOAD,
082        /** Image stored in base64 */
083        BASE64,
084        /** To be extracted from userpref */
085        USERPREF,
086        /** The default image */
087        DEFAULT
088    }
089    
090    /** The pref context for user profile */
091    public static final String USER_PROFILE_PREF_CONTEXT = "/profile";
092
093    /** The profile image user pref id */
094    public static final String USERPREF_PROFILE_IMAGE = "profile-image";
095    
096    /** Name of the avatar directory */
097    protected static final String __AVATAR_DIR_NAME = "avatar";
098    
099    /** Name of the initials directory */
100    protected static final String __INITIALS_DIR_NAME = "initials";
101    
102    /** The map of paths to avatar images, keys are id */
103    protected static Map<String, String> __avatarPaths;
104    
105    /** Ordered list of paths to available backgrounds for 'initials' images */
106    protected static List<String> __initialsBgPaths;
107    
108    /** Upload manager */
109    protected UploadManager _uploadManager;
110    
111    /** User pref manager */
112    protected UserPreferencesManager _userPreferencesManager;
113    
114    /** JSON Utils */
115    protected JSONUtils _jsonUtils;
116
117    private Context _context;
118    
119    @Override
120    public void service(ServiceManager smanager) throws ServiceException
121    {
122        super.service(smanager);
123        _uploadManager = (UploadManager) smanager.lookup(UploadManager.ROLE);
124        _userPreferencesManager = (UserPreferencesManager) smanager.lookup(UserPreferencesManager.ROLE); 
125        _jsonUtils = (JSONUtils) smanager.lookup(JSONUtils.ROLE);
126    }
127    
128    public void contextualize(Context context) throws ContextException
129    {
130        _context = context;
131    }
132    
133    @Override
134    public UserProfileImage getImage(UserIdentity user, String imageSource, int size, int maxSize) throws ProcessingException
135    {
136        ProfileImageSource profileImageSource = getProfileImageSource(imageSource);
137        if (profileImageSource == null)
138        {
139            profileImageSource = ProfileImageSource.USERPREF; // default
140        }
141        
142        // Get parameters for source
143        Map<String, Object> sourceParams = _extractSourceParameters(user, profileImageSource);
144        
145        UserProfileImage image = null;
146        if (sourceParams != null)
147        {
148            // Add size params
149            if (size > 0)
150            {
151                sourceParams.put("size", size);
152            }
153            if (maxSize > 0)
154            {
155                sourceParams.put("maxSize", maxSize);
156            }
157            
158            image = getImage(profileImageSource, user, sourceParams);
159            
160            if (image == null && ProfileImageSource.USERPREF.equals(profileImageSource))
161            {
162                // Reading from userpref, but no userpref set.
163                // Try gravatar, then initials
164                image = getGravatarImage(user, size > 0 ? size : maxSize);
165                if (image == null)
166                {
167                    image = getInitialsImage(user);
168                }
169            }
170        }
171        
172        if (image == null)
173        {
174            image = getDefaultImage();
175            
176            // still null?
177            if (image == null)
178            {
179                throw new ProcessingException(String.format("Not able to provide an image from source '%s' for user '%s' because no image was found.", profileImageSource, user));
180            }
181        }
182        
183        return image;
184    }
185    
186    
187    // TODO Cache: ametys home user-profiles? For remote image (gravatar) at least
188    
189    /**
190     * Get the profile image source given a source input string
191     * @param imageSourceStr The input string representing the source
192     * @return The profile image source.
193     */
194    public ProfileImageSource getProfileImageSource(String imageSourceStr)
195    {
196        ProfileImageSource profileImageSource = null;
197        
198        try
199        {
200            if (StringUtils.isNotEmpty(imageSourceStr))
201            {
202                profileImageSource = ProfileImageSource.valueOf(imageSourceStr.toUpperCase());
203            }
204        }
205        catch (IllegalArgumentException e)
206        {
207            if (getLogger().isWarnEnabled())
208            {
209                getLogger().warn("Unknown profile image source " + imageSourceStr + ".", e);
210            }
211        }
212        
213        return profileImageSource;
214    }
215    
216    /**
217     * Provides the necessary parameters to retrieves the image from a given source.
218     * @param user The user
219     * @param profileImageSource The image source type
220     * @return A map of parameters
221     * @throws ResourceNotFoundException In case of a unhandled source type or if parameters could not be extracted 
222     */
223    
224    protected Map<String, Object> _extractSourceParameters(UserIdentity user, ProfileImageSource profileImageSource) throws ResourceNotFoundException
225    {
226        Request request = ContextHelper.getRequest(_context);
227        
228        switch (profileImageSource)
229        {
230            case UPLOAD:
231                return _extractUploadParameters(request, user);
232            case LOCALIMAGE:
233                return _extractLocalImageParameters(request, user);
234            case BASE64:
235                return _extractBase64Parameters(request, user);
236            case INITIALS:
237            case USERSMANAGER:
238            case USERPREF:
239            case GRAVATAR:
240            case DEFAULT:
241                // nothing special
242                return new HashMap<>();
243            default:
244                if (getLogger().isWarnEnabled())
245                {
246                    getLogger().warn(String.format("Cannot extract image source parameters for user '%s'. Unhandled profile image source '%s'", user, profileImageSource));
247                }
248                return null;
249        }
250    }
251    
252    /**
253     * Extracts parameters for an uploaded image
254     * @param request The request
255     * @param user The user
256     * @return A map containing the uploaded file id (key=id)
257     */
258    protected Map<String, Object> _extractUploadParameters(Request request, UserIdentity user)
259    {
260        String uploadId = request.getParameter("id");
261        
262        if (StringUtils.isEmpty(uploadId))
263        {
264            getLogger().error("Missing mandatory uploaded file id parameter to retrieve the uploaded file for user " + user + ".");
265            return null;
266        }
267        
268        Map<String, Object> params = new HashMap<>();
269        params.put("id", uploadId);
270        
271        return params;
272    }
273    
274    /**
275     * Extracts parameters for a local image
276     * @param request The request
277     * @param user The user
278     * @return A map containing the local image id (key=id)
279     */
280    protected Map<String, Object> _extractLocalImageParameters(Request request, UserIdentity user)
281    {
282        String localFileId = request.getParameter("id");
283        
284        if (StringUtils.isEmpty(localFileId))
285        {
286            getLogger().error("Missing mandatory local file id parameter to retrieve the local file for user " + user + ".");
287            return null;
288        }
289        
290        Map<String, Object> params = new HashMap<>();
291        params.put("id", localFileId);
292        
293        return params;
294    }
295    
296    /**
297     * Extracts parameters for a local image
298     * @param request The request
299     * @param user The user
300     * @return A map containing the local image id (key=id)
301     */
302    protected Map<String, Object> _extractBase64Parameters(Request request, UserIdentity user)
303    {
304        String data = request.getParameter("data");
305        
306        if (StringUtils.isEmpty(data))
307        {
308            getLogger().error("Missing mandatory data parameter for user image of type base 64 user " + user + ".");
309            return null;
310        }
311        
312        Map<String, Object> params = new HashMap<>();
313        params.put("data", data);
314        
315        String filename = request.getParameter("filename");
316        if (StringUtils.isNotEmpty(filename))
317        {
318            params.put("filename", filename);
319        }
320        
321        return params;
322    }
323    
324    /**
325     * Get the image input stream
326     * @param source The image source type
327     * @param user The user
328     * @param sourceParams The parameters used by the source
329     * @return The UserProfileImage for the image or null if not found
330     */
331    public UserProfileImage getImage(ProfileImageSource source, UserIdentity user, Map<String, Object> sourceParams)
332    {
333        switch (source)
334        {
335            case USERPREF:
336                return getUserPrefImage(user, sourceParams);
337            case GRAVATAR:
338                return getGravatarImage(user, _getGravatarSize(sourceParams));
339            case UPLOAD:
340                return getUploadedImage(user, (String) sourceParams.get("id"));
341            case LOCALIMAGE:
342                return getLocalImage(user, (String) sourceParams.get("id"));
343            case INITIALS:
344                return getInitialsImage(user);
345            case BASE64:
346                return getBase64Image(user, (String) sourceParams.get("data"), (String) sourceParams.get("filename"));
347            case DEFAULT:
348                return getDefaultImage();
349            case USERSMANAGER:
350                // not implemented yet
351            default:
352                if (getLogger().isWarnEnabled())
353                {
354                    getLogger().warn(String.format("Cannot get image for user '%s'. Unhandled profile image source '%s'", user, source));
355                }
356        }
357        
358        return null;
359    }
360    
361    /**
362     * Get the image from a base 64 string
363     * @param user The user
364     * @param data The base64 data representing the image
365     * @param filename The filename or null if not known
366     * @return The UserProfileImage for the image or null if not set
367     */
368    public UserProfileImage getBase64Image(UserIdentity user, String data, String filename)
369    {
370        if (StringUtils.isEmpty(data))
371        {
372            if (getLogger().isWarnEnabled())
373            {
374                getLogger().warn(String.format("No data provided. Unable to retrieve the base64 image for user '%s'.", user));
375            }
376            return null;
377        }
378        
379        InputStream is = new ByteArrayInputStream(new Base64(true).decode(data));
380        return new UserProfileImage(is, StringUtils.defaultIfBlank(filename, null), null);
381    }
382    
383    
384    /**
385     * Extract the gravatar size from the source params if any
386     * @param sourceParams The source params
387     * @return The requested image size for gravatar or null if not provided
388     */
389    private Integer _getGravatarSize(Map<String, Object> sourceParams)
390    {
391        Integer size = (Integer) sourceParams.get("size");
392        if (size != null && size > 0)
393        {
394            return size;
395        }
396        
397        Integer maxSize = (Integer) sourceParams.get("maxSize");
398        if (maxSize != null && maxSize > 0)
399        {
400            return maxSize;
401        }
402        
403        return null;
404    }
405    
406    /**
407     * Get the image from the user pref
408     * @param user The user
409     * @param baseSourceParams The base source params to be merge with the params stored in the user pref
410     * @return The UserProfileImage for the image or null if not set
411     */
412    public UserProfileImage getUserPrefImage(UserIdentity user, Map<String, Object> baseSourceParams)
413    {
414        Map<String, Object> userPrefImgData = _getRawUserPrefImage(user);
415        if (userPrefImgData != null)
416        {
417            String rawImageSource = (String) userPrefImgData.remove("source");
418            ProfileImageSource profileImageSource = getProfileImageSource(rawImageSource);
419            
420            if (profileImageSource == null || ProfileImageSource.USERPREF.equals(profileImageSource))
421            {
422                if (getLogger().isWarnEnabled())
423                {
424                    getLogger().warn("An profile image seems to be stored as an userpref but its image source is empty, not handled or corrupted");
425                }
426                return null;
427            }
428            
429            @SuppressWarnings("unchecked")
430            Map<String, Object> sourceParams = (Map<String, Object>) userPrefImgData.get("parameters");
431            if (sourceParams != null)
432            {
433                sourceParams.putAll(baseSourceParams);
434            }
435            else
436            {
437                sourceParams = new HashMap<>(baseSourceParams);
438            }
439            
440            return getImage(profileImageSource, user, sourceParams);
441        }
442        
443        return null;
444    }
445    
446    /**
447     * Test this user as a profile image set in its user pref
448     * @param user The user
449     * @return The map stored in the user pref
450     */
451    public Map<String, Object> hasUserPrefImage(UserIdentity user)
452    {
453        Map<String, Object> userPrefImgData = _getRawUserPrefImage(user);
454        if (userPrefImgData != null)
455        {
456            String rawImageSource = (String) userPrefImgData.get("source");
457            ProfileImageSource profileImageSource = getProfileImageSource(rawImageSource);
458            if (profileImageSource != null)
459            {
460                return userPrefImgData;
461            }
462        }
463        
464        return null;
465    }
466    
467    /**
468     * Get the profile image user pref
469     * @param user The user
470     * @return The map stored in the user pref
471     */
472    private Map<String, Object> _getRawUserPrefImage(UserIdentity user)
473    {
474        try
475        {
476            String userPrefImgJson = _userPreferencesManager.getUserPreferenceAsString(user, USER_PROFILE_PREF_CONTEXT, Collections.EMPTY_MAP, USERPREF_PROFILE_IMAGE);
477            if (StringUtils.isNotEmpty(userPrefImgJson))
478            {
479                return _jsonUtils.convertJsonToMap(userPrefImgJson);
480            }
481        }
482        catch (Exception e)
483        {
484            getLogger().error(String.format("Unable to retrieve the '%s' userpref on context '%s' for user '%s'", USERPREF_PROFILE_IMAGE, USER_PROFILE_PREF_CONTEXT, user), e);
485        }
486        
487        return null;
488    }
489    
490    /**
491     * Get the uploaded image
492     * @param user The user
493     * @param uploadId The upload identifier
494     * @return The UserProfileImage for the image or null if not found
495     */
496    public UserProfileImage getUploadedImage(UserIdentity user, String uploadId)
497    {
498        if (StringUtils.isEmpty(uploadId))
499        {
500            return null;
501        }
502        
503        Upload upload = null;
504        try
505        {
506            upload = _uploadManager.getUpload(user, uploadId);
507            try (InputStream is = upload.getInputStream())
508            {
509                BufferedImage croppedImage = cropUploadedImage(is);
510                
511                String filename =  upload.getFilename();
512                String format = FilenameUtils.getExtension(filename);
513                format = ProfileImageReader.ALLOWED_IMG_FORMATS.contains(format) ? format : "png";
514                
515                try (ByteArrayOutputStream baos = new ByteArrayOutputStream())
516                {
517                    ImageIO.write(croppedImage, format, baos);
518                    return new UserProfileImage(new ByteArrayInputStream(baos.toByteArray()), filename, null); // no length because image is cropped
519                }
520            }
521            catch (IOException e)
522            {
523                getLogger().error(String.format("Unable to provide the uploaded cropped image for user '%s'and upload id '%s'.", user, uploadId), e); 
524            }
525        }
526        catch (NoSuchElementException e)
527        {
528            // Invalid upload id
529            getLogger().error(String.format("Cannot find the temporary uploaded file for id '%s' and login '%s'.", uploadId, user), e);
530        }
531        
532        return null;
533    }
534    
535    /**
536     * Automatically crop the image to 64x64 pixels.
537     * @param is The input stream of the uploaded file
538     * @return The base64 string
539     * @throws IOException If an exception occurs while manipulating streams
540     */
541    public BufferedImage cropUploadedImage(InputStream is) throws IOException
542    {
543        // Crop the image the get a square image, vertically centered to the input image.
544        BufferedImage image = ImageHelper.read(is);
545        int width = image.getWidth();
546        int height = image.getHeight();
547        if (width != height)
548        {
549            int min = Math.min(width, height);
550            image = image.getSubimage((width - min) / 2, 0, min, min);
551        }
552        
553        // Scale square image to side of 64px 
554        return ImageHelper.generateThumbnail(image, 0, 0, 64, 64);
555    }
556    
557    /**
558     * Test if the local image exists
559     * @param localFileId The local file identifier
560     * @return True if the image exists
561     */
562    public boolean hasLocalImage(String localFileId)
563    {
564        Source imgSource = null;
565        try
566        {
567            imgSource = _getLocalImageSource(localFileId);
568            if (imgSource == null)
569            {
570                if (getLogger().isWarnEnabled())
571                {
572                    getLogger().warn(String.format("Unable to test the local image for id '%s.", localFileId));
573                }
574                return false;
575            }
576            
577            return imgSource.exists();
578        }
579        catch (IOException e)
580        {
581            getLogger().error(String.format("Unable to  test the local image for id '%s' and login '%s'.", localFileId), e);
582        }
583        finally
584        {
585            if (imgSource != null)
586            {
587                _sourceResolver.release(imgSource);
588            }
589        }
590        
591        return false;
592    }
593    
594    /**
595     * Get the local image
596     * @param user The user
597     * @param localFileId The local file identifier
598     * @return The UserProfileImage for the image or null if not found
599     */
600    public UserProfileImage getLocalImage(UserIdentity user, String localFileId)
601    {
602        Source imgSource = null;
603        
604        try
605        {
606            imgSource = _getLocalImageSource(localFileId);
607            
608            if (imgSource == null)
609            {
610                if (getLogger().isWarnEnabled())
611                {
612                    getLogger().warn(String.format("Unable to retrieve the local image for id '%s' and login '%s'.", localFileId, user));
613                }
614                return null;
615            }
616            else if (imgSource.exists())
617            {
618                String avatarPath = _getLocalImagePaths().get(localFileId);
619                return new UserProfileImage(imgSource.getInputStream(), FilenameUtils.getName(avatarPath), null);
620            }
621            
622            if (getLogger().isWarnEnabled())
623            {
624                getLogger().warn(String.format("Unable to find any local image with id '%s' for user '%s'", localFileId, user));
625            }
626        }
627        catch (IOException e)
628        {
629            getLogger().error(String.format("Unable to retrieve the local image for id '%s' and login '%s'.", localFileId, user), e);
630        }
631        finally
632        {
633            if (imgSource != null)
634            {
635                _sourceResolver.release(imgSource);
636            }
637        }
638        
639        return null;
640    }
641    
642    /**
643     * Get the source of a local image
644     * @param localFileId The local file identifier
645     * @return The source or null
646     * @throws IOException If an error occurs while resolving the source uri
647     */
648    protected Source _getLocalImageSource(String localFileId) throws IOException
649    {
650        Map<String, String> imgPaths = _getLocalImagePaths();
651        String avatarPath = imgPaths != null ? imgPaths.get(localFileId) : StringUtils.EMPTY;
652        
653        if (StringUtils.isEmpty(avatarPath))
654        {
655            return null;
656        }
657        
658        String location = "plugin:core-ui://resources/img/" + __USER_PROFILES_DIR_PATH + "/" + __AVATAR_DIR_NAME + "/" + avatarPath;
659        return _sourceResolver.resolveURI(location);
660    }
661    
662    /**
663     * Get the list of local image identifiers
664     * @return Ordered list of identifiers
665     */
666    public List<String> getLocalImageIds()
667    {
668        return new LinkedList<>(_getLocalImagePaths().keySet());
669    }
670    
671    /**
672     * Get the map containing the relative path for each local image.
673     * Create the map if not existing yet.
674     * @return Map where keys are ids and values are the relative paths
675     */
676    protected Map<String, String> _getLocalImagePaths()
677    {
678        _initializeLocalImagePaths();
679        return __avatarPaths;
680    }
681    
682    /**
683     * Initializes the map of local image paths
684     */
685    private void _initializeLocalImagePaths()
686    {
687        synchronized (DefaultProfileImageProvider.class)
688        {
689            if (__avatarPaths == null)
690            {
691                __avatarPaths = new LinkedHashMap<>(); // use insertion order
692                
693                String location = "plugin:core-ui://resources/img/" + __USER_PROFILES_DIR_PATH + "/" + __AVATAR_DIR_NAME + "/" + __AVATAR_DIR_NAME + ".xml";
694                Source source = null;
695                try
696                {
697                    source = _sourceResolver.resolveURI(location);
698                    
699                    try (InputStream is = source.getInputStream())
700                    {
701                        Configuration cfg = new DefaultConfigurationBuilder().build(is);
702                        for (Configuration imageCfg : cfg.getChildren("image"))
703                        {
704                            __avatarPaths.put(imageCfg.getAttribute("id"), imageCfg.getValue());
705                        }
706                    }
707                }
708                catch (IOException | ConfigurationException | SAXException e)
709                {
710                    getLogger().error("Unable to retrieve the map of local image paths", e);
711                }
712                finally
713                {
714                    if (source != null)
715                    {
716                        _sourceResolver.release(source);
717                    }
718                }
719            }
720        }
721    }
722    
723    /**
724     * Test if the initials image is available for a given user
725     * @param userIdentity The user
726     * @return True if the image exists
727     */
728    public boolean hasInitialsImage(UserIdentity userIdentity)
729    {
730        User user = _userManager.getUser(userIdentity.getPopulationId(), userIdentity.getLogin());
731        if (user == null)
732        {
733            getLogger().warn("Unable to test the initials image - user not found " + userIdentity);
734            return false;
735        }
736        
737        String initial = user.getFullName().substring(0, 1).toLowerCase();
738        Source imgSource = null;
739        
740        try
741        {
742            imgSource = _getInitialsImageSource(initial);
743            return imgSource.exists();
744        }
745        catch (IOException e)
746        {
747            getLogger().error(String.format("Unable to test initials image for user '%s' with fullname '%s'.", userIdentity, user.getFullName()), e);
748        }
749        finally
750        {
751            if (imgSource != null)
752            {
753                _sourceResolver.release(imgSource);
754            }
755        }
756        
757        return false;
758    }
759    
760    /**
761     * Get the image with user initials
762     * @param userIdentity The user
763     * @return The UserProfileImage for the image or null if not found
764     */
765    public UserProfileImage getInitialsImage(UserIdentity userIdentity)
766    {
767        User user = _userManager.getUser(userIdentity.getPopulationId(), userIdentity.getLogin());
768        if (user == null)
769        {
770            getLogger().warn("Unable to get initials image - user not found " + userIdentity);
771            return null;
772        }
773        
774        String initial = user.getFullName().substring(0, 1).toLowerCase();
775        Source imgSource = null;
776        
777        try
778        {
779            imgSource = _getInitialsImageSource(initial);
780            if (imgSource.exists())
781            {
782                String filename = initial + ".png";
783                try (InputStream is = imgSource.getInputStream())
784                {
785                    try
786                    {
787                        InputStream imageIsWithBackground = _addImageBackground(userIdentity, is);
788                        return new UserProfileImage(imageIsWithBackground, filename, null);
789                    }
790                    catch (IOException e)
791                    {
792                        getLogger().error(
793                                String.format("Unable to add the background image to the initials image for user '%s' with fullname '%s'. Only the initial image will be used.",
794                                        userIdentity, user.getFullName()), e);
795                        
796                        
797                        // Return image without background
798                        _sourceResolver.release(imgSource);
799                        imgSource = _getInitialsImageSource(initial);
800                        return new UserProfileImage(imgSource.getInputStream(), filename, null);
801                    }
802                }
803            }
804            
805            if (getLogger().isWarnEnabled())
806            {
807                getLogger().warn(String.format("Unable to find the initials image for user '%s' with fullname '%s'", userIdentity, user.getFullName()));
808            }
809        }
810        catch (IOException e)
811        {
812            getLogger().error(String.format("Unable to retrieve the initials image for user '%s' with fullname '%s'.", userIdentity, user.getFullName()), e);
813        }
814        finally
815        {
816            if (imgSource != null)
817            {
818                _sourceResolver.release(imgSource);
819            }
820        }
821        
822        return null;
823    }
824    
825    /**
826     * Get the source of the initials image
827     * @param initial The initial
828     * @return The source
829     * @throws IOException If an error occurs while resolving the source uri
830     */
831    protected Source _getInitialsImageSource(String initial) throws IOException
832    {
833        String location = "plugin:core-ui://resources/img/" + __USER_PROFILES_DIR_PATH + "/" + __INITIALS_DIR_NAME + "/" + initial + ".png";
834        return _sourceResolver.resolveURI(location);
835    }
836    
837    /**
838     * Add a background to an initials image
839     * @param user The user used to determine which background will be used (based on a hash representation of the login)
840     * @param is The inputstream of the image
841     * @return The inputstream of the final image with the background
842     * @throws IOException If any sort of IO error occurs during the process 
843     */
844    protected InputStream _addImageBackground(UserIdentity user, InputStream is) throws IOException
845    {
846        BufferedImage image = ImageIO.read(is);
847        Source bgSource = null;
848        try
849        {
850            bgSource = _getInitialsBackgroundSource(user);
851            BufferedImage background = null;
852            
853            try (InputStream backgroundIs = bgSource.getInputStream())
854            {
855                background = ImageIO.read(backgroundIs);
856                Graphics backgroundGraphics = background.getGraphics();
857                backgroundGraphics.drawImage(image, 0, 0, null);
858            }
859            
860            try (ByteArrayOutputStream baos = new ByteArrayOutputStream())
861            {
862                ImageIO.write(background, "png", baos);
863                return new ByteArrayInputStream(baos.toByteArray());
864            }
865        }
866        finally
867        {
868            _sourceResolver.release(bgSource);
869        }
870    }
871    
872    /**
873     * Get the background image for the initials source.
874     * The chosen background depend on the user login 
875     * @param user The user
876     * @return The source
877     * @throws IOException If an error occurs while resolving the source uri
878     */
879    protected Source _getInitialsBackgroundSource(UserIdentity user) throws IOException
880    {
881        // Hashing the login then choose a background given the available ones.
882        long hash = Math.abs(HashUtil.hash(user.getLogin()));
883        
884        // Perform a modulo on the hash given number of available background
885        _initializeInitialsBackgroundPaths();
886        long nbBackground = __initialsBgPaths.size();
887        if (nbBackground == 0)
888        {
889            throw new IOException("No backgrounds available.");
890        }
891        
892        int indexBackground = (int) (hash % nbBackground);
893        
894        // Get file from list
895        String path = __initialsBgPaths.get(indexBackground);
896        
897        String location = "plugin:core-ui://resources/img/" + __USER_PROFILES_DIR_PATH + "/" + __INITIALS_DIR_NAME + "/" + path;
898        return _sourceResolver.resolveURI(location);
899    }
900    
901    /**
902     * Initializes the list of background paths for initials images
903     */
904    private void _initializeInitialsBackgroundPaths()
905    {
906        synchronized (DefaultProfileImageProvider.class)
907        {
908            if (__initialsBgPaths == null)
909            {
910                __initialsBgPaths = new LinkedList<>();
911                
912                String location = "plugin:core-ui://resources/img/" + __USER_PROFILES_DIR_PATH + "/" + __INITIALS_DIR_NAME + "/" + __INITIALS_DIR_NAME + ".xml";
913                Source source = null;
914                
915                try
916                {
917                    source = _sourceResolver.resolveURI(location);
918                    
919                    try (InputStream is = source.getInputStream())
920                    {
921                        Configuration cfg = new DefaultConfigurationBuilder().build(is);
922                        
923                        for (Configuration backgroundCfg : cfg.getChildren("background"))
924                        {
925                            __initialsBgPaths.add(backgroundCfg.getValue());
926                        }
927                    }
928                }
929                catch (IOException | ConfigurationException | SAXException e)
930                {
931                    getLogger().error("Unable to retrieve the list of available backgrounds for initials images", e);
932                }
933                finally
934                {
935                    if (source != null)
936                    {
937                        _sourceResolver.release(source);
938                    }
939                }
940            }
941        }
942    }
943}
944