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