Class UserProfileService

java.lang.Object
com.xebialabs.xlrelease.service.UserProfileService

@Service public class UserProfileService extends Object
  • Field Details

    • ROOT

      public static final String ROOT
  • Constructor Details

    • UserProfileService

      @Autowired public UserProfileService(com.xebialabs.xlrelease.repository.UserProfileRepository userProfileRepository, PrincipalDataProvider principalDataProvider, com.xebialabs.license.service.LicenseService licenseService, SessionService sessionService, com.xebialabs.xlrelease.events.EventBus eventBus, WelcomeTemplateHandler welcomeTemplateHandler, com.xebialabs.xlrelease.config.XlrConfig xlrConfig)
  • Method Details

    • hasExternalPropertiesChanged

      public static boolean hasExternalPropertiesChanged(com.xebialabs.xlrelease.domain.UserProfile original, com.xebialabs.xlrelease.domain.UserProfile updated)
    • findAll

      public List<com.xebialabs.xlrelease.domain.UserProfile> findAll(Boolean fullProfile)
    • deleteByUsername

      public void deleteByUsername(String username)
    • countUserWithLoginAllowed

      public int countUserWithLoginAllowed()
    • areLicensesAvailable

      public boolean areLicensesAvailable()
    • search

      public List<com.xebialabs.xlrelease.domain.UserProfile> search(String email, String fullName, Boolean loginAllowed, Date lastActiveAfter, Date lastActiveBefore, Long page, Long resultsPerPage)
    • searchUserAccounts

      public org.springframework.data.domain.Page<com.xebialabs.xlrelease.domain.UserProfile> searchUserAccounts(com.xebialabs.xlrelease.views.users.UserFilters userFilters, org.springframework.data.domain.Pageable pageable)
    • findByUsername

      public com.xebialabs.xlrelease.domain.UserProfile findByUsername(String username)
    • ensureCreated

      public void ensureCreated(String username)
    • discover

      public com.xebialabs.xlrelease.domain.UserProfile discover(String username)
    • resolveUserProfile

      public com.xebialabs.xlrelease.domain.UserProfile resolveUserProfile(String username)
    • resolveUserProfile

      public com.xebialabs.xlrelease.domain.UserProfile resolveUserProfile(String username, boolean resolveWithDataProvider)
    • createOrUpdate

      public com.xebialabs.xlrelease.domain.UserProfile createOrUpdate(String username)
    • save

      public void save(com.xebialabs.xlrelease.domain.UserProfile profile)
    • updateProfile

      public void updateProfile(com.xebialabs.xlrelease.domain.UserProfile... profiles)
    • updateLastActive

      public boolean updateLastActive(String canonicalId, Date lastActive, Boolean cacheEvict)
    • findByUsernameForUpdate

      public com.xebialabs.xlrelease.domain.UserProfile findByUsernameForUpdate(String canonicalId)
      Retrieves user profile with pessimistic row-level lock (FOR UPDATE).

      ⚠️ MUST be called within an active transaction. The UPDATE must occur in the SAME transaction:

      Purpose:

      This method prevents race conditions during concurrent failed login attempts by:
      1. Acquiring an exclusive row lock (FOR UPDATE / WITH (UPDLOCK, ROWLOCK))
      2. Blocking other transactions from reading/modifying the same row
      3. Ensuring atomic read-modify-write operations

      Correct Usage Example:

      
       @Transactional(propagation = REQUIRED, isolation = READ_COMMITTED)
       public void recordFailedAttempt(String username) {
         UserProfile profile = userProfileService.findByUsernameForUpdate(username);  // Acquires lock
         int newCount = profile.getLastFailedLoginAttemptCount() + 1;
         userProfileService.updateFailedLoginAttempt(username, newCount, ...);  // Same transaction
         // Lock released on commit
       }
       

      ❌ INCORRECT Usage (Will cause data inconsistency):

      
       // DON'T DO THIS - No transaction!
       val profile = userProfilePersistence.findByUsernameForUpdate(username)
       // Lock released here, race condition possible
       Thread.sleep(1000) // Another thread could modify data
       userProfilePersistence.updateFailedLoginAttempt(...) // Too late!
      
       // DON'T DO THIS - Separate transactions!
       @Transactional
       def read() = userProfilePersistence.findByUsernameForUpdate(username)
      
       @Transactional // New transaction = new lock = race condition
       def update() = userProfilePersistence.updateFailedLoginAttempt(...)
       

      Database-Specific Locking:

      • PostgreSQL/MySQL/Oracle: Uses standard FOR UPDATE clause
      • SQL Server: Uses WITH (UPDLOCK, ROWLOCK) hint
      Parameters:
      canonicalId - The username to lock and retrieve
      Returns:
      UserProfile with account lock data, or null if not found
    • updateFailedLoginAttempt

      public int updateFailedLoginAttempt(String canonicalId, int newCount, Boolean newLocked, Date lastFailedAttemptAt, Boolean cacheEvict)
    • updateLastActiveBatch

      public int updateLastActiveBatch(Map<String,Date> entries)
    • validate

      public void validate(com.xebialabs.xlrelease.domain.UserProfile profile)