Skip to content

Commit 53b74ae

Browse files
authored
Merge branch 'issue-134-Add-SessionProfile-scaffolding-for-consuming-applications' into merge-fix-1
2 parents 410adc5 + 1fce50d commit 53b74ae

8 files changed

Lines changed: 363 additions & 10 deletions

File tree

gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
version=3.0.2-SNAPSHOT
1+
version=3.1.0-SNAPSHOT
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package com.digitalsanctuary.spring.user;
2+
3+
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
4+
import org.springframework.boot.autoconfigure.AutoConfigurationPackages;
5+
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
6+
import org.springframework.core.type.AnnotationMetadata;
7+
8+
/**
9+
* {@code UserAutoConfigurationRegistrar} dynamically registers the base package of this library with Spring Boot to ensure that its entities,
10+
* repositories, and other Spring-managed components are properly detected and included in the application context.
11+
*
12+
* <p>
13+
* This class is designed to simplify the integration of the library into Spring Boot applications by automatically registering the library's base
14+
* package (<i>com.digitalsanctuary.spring.user</i>) for component scanning. It ensures that:
15+
* <ul>
16+
* <li>The library's repositories and entities are discovered and configured correctly.</li>
17+
* <li>The consuming application retains its ability to automatically detect its own repositories and entities.</li>
18+
* </ul>
19+
*
20+
* <p>
21+
* This approach avoids the need for the consuming application to manually specify the library's base package or manage complex configuration,
22+
* reducing setup effort and minimizing potential errors.
23+
*
24+
* <p>
25+
* <b>Note:</b> This solution leverages {@link AutoConfigurationPackages#register} to dynamically register the library's package during the
26+
* auto-configuration phase, ensuring compatibility with Spring Boot's component scanning and auto-configuration mechanisms.
27+
*/
28+
public class UserAutoConfigurationRegistrar implements ImportBeanDefinitionRegistrar {
29+
30+
/**
31+
* Registers the library's base package (<i>com.digitalsanctuary.spring.user</i>) with the Spring application context to enable automatic
32+
* detection of entities, repositories, and other components provided by the library.
33+
*
34+
* @param importingClassMetadata metadata of the class that imports this registrar
35+
* @param registry the bean definition registry used to register the base package
36+
*/
37+
@Override
38+
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
39+
// Register the top-level package for the library
40+
AutoConfigurationPackages.register(registry, "com.digitalsanctuary.spring.user");
41+
}
42+
}
Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
package com.digitalsanctuary.spring.user;
22

3-
import org.springframework.boot.autoconfigure.domain.EntityScan;
43
import org.springframework.context.annotation.ComponentScan;
54
import org.springframework.context.annotation.Configuration;
6-
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
5+
import org.springframework.context.annotation.Import;
76
import org.springframework.scheduling.annotation.EnableAsync;
87
import org.springframework.scheduling.annotation.EnableScheduling;
98
import jakarta.annotation.PostConstruct;
@@ -19,18 +18,17 @@
1918
@EnableAsync
2019
@EnableScheduling
2120
@ComponentScan(basePackages = "com.digitalsanctuary.spring.user")
22-
@EnableJpaRepositories(basePackages = "com.digitalsanctuary.spring.user.persistence.repository")
23-
@EntityScan(basePackages = "com.digitalsanctuary.spring.user.persistence.model")
21+
@Import(UserAutoConfigurationRegistrar.class)
2422
public class UserConfiguration {
2523

24+
2625
/**
27-
* Method executed after the bean initialization.
28-
* <p>
29-
* This method logs a message indicating that the DigitalSanctuary Spring Boot User Framework LIbrary has been loaded.
30-
* </p>
26+
* Logs a message when the UserConfiguration class is loaded to indicate that the DigitalSanctuary Spring Boot User Framework Library has been
27+
* loaded.
3128
*/
3229
@PostConstruct
3330
public void onStartup() {
34-
log.info("DigitalSanctuary SpringBoot User Framework Library loaded");
31+
log.info("DigitalSanctuary SpringBoot User Framework Library loaded.");
3532
}
33+
3634
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package com.digitalsanctuary.spring.user.profile;
2+
3+
import java.time.LocalDateTime;
4+
import com.digitalsanctuary.spring.user.persistence.model.User;
5+
import jakarta.persistence.Column;
6+
import jakarta.persistence.Id;
7+
import jakarta.persistence.JoinColumn;
8+
import jakarta.persistence.MappedSuperclass;
9+
import jakarta.persistence.MapsId;
10+
import jakarta.persistence.OneToOne;
11+
import lombok.Data;
12+
13+
/**
14+
* Base class for user profile entities that extend the core {@link User} functionality. This class provides the foundation for creating
15+
* application-specific user profiles with shared common attributes.
16+
*
17+
* <p>
18+
* This class uses {@code @MappedSuperclass} to allow inheritance of JPA mappings, enabling extending classes to add additional fields while
19+
* maintaining a consistent database structure. The profile shares its primary key with the associated {@link User} entity through the {@code @MapsId}
20+
* annotation.
21+
* </p>
22+
*
23+
* Example implementation: {@code @Entity
24+
*
25+
* @Table(name = "customer_profile") public class CustomerProfile extends BaseUserProfile { private String customerType; private String
26+
* shippingPreference; private List<Order> orders; } }
27+
*
28+
* Database Structure: - id/user_id (PK/FK to user_account table) - last_accessed (timestamp) - preferred_locale (varchar)
29+
*
30+
* @see User
31+
* @see MappedSuperclass
32+
*/
33+
@Data
34+
@MappedSuperclass
35+
public abstract class BaseUserProfile {
36+
37+
/**
38+
* The primary key for the profile, shared with the associated User entity. This is automatically populated through the {@code @MapsId} annotation
39+
* when the profile is persisted.
40+
*/
41+
@Id
42+
private Long id;
43+
44+
/**
45+
* The associated User entity. This establishes a one-to-one relationship with shared primary key through the {@code @MapsId} annotation. The
46+
* foreign key column will be named "user_id".
47+
*/
48+
@OneToOne
49+
@MapsId
50+
@JoinColumn(name = "user_id")
51+
private User user;
52+
53+
/**
54+
* Timestamp of the last time this profile was accessed. This can be used for tracking user activity, session management, or implementing timeout
55+
* functionality.
56+
*/
57+
@Column(name = "last_accessed")
58+
private LocalDateTime lastAccessed;
59+
60+
/**
61+
* The user's preferred locale for internationalization purposes. This should contain a valid locale code (e.g., "en_US", "fr_FR"). Applications
62+
* can use this to provide localized content and formatting.
63+
*/
64+
@Column(name = "preferred_locale")
65+
private String locale;
66+
67+
// Note: Getters and setters are provided by Lombok @Data annotation
68+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package com.digitalsanctuary.spring.user.profile;
2+
3+
import com.digitalsanctuary.spring.user.persistence.model.User;
4+
5+
/**
6+
* Service interface for managing user profiles. This interface defines the core operations for retrieving, creating, and updating user profiles that
7+
* extend the base profile functionality.
8+
*
9+
* <p>
10+
* Implementations of this interface handle the persistence and business logic for user profiles, providing a standardized way to manage extended user
11+
* data beyond the core {@link User} entity.
12+
* </p>
13+
*
14+
* Example implementation: {@code @Service public class CustomUserProfileService implements UserProfileService<CustomUserProfile> { private final
15+
* CustomUserProfileRepository profileRepository;
16+
*
17+
* @Override public CustomUserProfile getOrCreateProfile(User user) { return profileRepository.findByUserId(user.getId()) .orElseGet(() -> {
18+
* CustomUserProfile profile = new CustomUserProfile(); profile.setUser(user); return profileRepository.save(profile); }); }
19+
*
20+
* @Override public CustomUserProfile updateProfile(CustomUserProfile profile) { return profileRepository.save(profile); } } }
21+
*
22+
* @param <T> the type of user profile to manage, must extend BaseUserProfile
23+
* @see BaseUserProfile
24+
* @see User
25+
*/
26+
public interface UserProfileService<T extends BaseUserProfile> {
27+
28+
/**
29+
* Retrieves an existing profile for the given user or creates a new one if none exists. This method ensures that every user has an associated
30+
* profile.
31+
*
32+
* <p>
33+
* Implementations should:
34+
* </p>
35+
* <ul>
36+
* <li>Check if a profile exists for the user</li>
37+
* <li>Create a new profile if none exists</li>
38+
* <li>Initialize any required default values for new profiles</li>
39+
* <li>Persist the profile if newly created</li>
40+
* </ul>
41+
*
42+
* @param user the user to get or create a profile for
43+
* @return the existing or newly created profile
44+
* @throws IllegalArgumentException if user is null
45+
* @throws RuntimeException if profile creation or retrieval fails
46+
*/
47+
T getOrCreateProfile(User user);
48+
49+
/**
50+
* Updates an existing user profile with new information.
51+
*
52+
* <p>
53+
* Implementations should:
54+
* </p>
55+
* <ul>
56+
* <li>Validate the profile data before updating</li>
57+
* <li>Persist the changes to the data store</li>
58+
* <li>Return the updated profile instance</li>
59+
* </ul>
60+
*
61+
* @param profile the profile to update
62+
* @return the updated profile
63+
* @throws IllegalArgumentException if profile is null or invalid
64+
* @throws RuntimeException if profile update fails
65+
*/
66+
T updateProfile(T profile);
67+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package com.digitalsanctuary.spring.user.profile.session;
2+
3+
import org.springframework.context.ApplicationListener;
4+
import org.springframework.security.authentication.event.InteractiveAuthenticationSuccessEvent;
5+
import org.springframework.stereotype.Component;
6+
import com.digitalsanctuary.spring.user.profile.BaseUserProfile;
7+
import com.digitalsanctuary.spring.user.profile.UserProfileService;
8+
import com.digitalsanctuary.spring.user.service.DSUserDetails;
9+
10+
/**
11+
* Base authentication listener that handles successful user authentication events by loading or creating the appropriate user profile and storing it
12+
* in the session. This class provides the core functionality for maintaining user profile state across the application session.
13+
*
14+
* <p>
15+
* This listener automatically responds to successful interactive authentication events (like form login) by retrieving or creating a user profile via
16+
* the {@link UserProfileService} and storing it in the session-scoped {@link BaseSessionProfile}.
17+
* </p>
18+
*
19+
* <p>
20+
* Example implementation:
21+
* </p>
22+
*
23+
* <pre>
24+
* {@code
25+
* @Component
26+
* public class CustomAuthenticationListener extends BaseAuthenticationListener<CustomUserProfile> {
27+
* public CustomAuthenticationListener(CustomSessionProfile sessionProfile, CustomUserProfileService profileService) {
28+
* super(sessionProfile, profileService);
29+
* }
30+
* }
31+
* }</pre>
32+
*
33+
* @param <T> the type of user profile, must extend BaseUserProfile
34+
*
35+
* @see BaseSessionProfile
36+
* @see UserProfileService
37+
* @see InteractiveAuthenticationSuccessEvent
38+
* @see DSUserDetails
39+
*/
40+
@Component
41+
public abstract class BaseAuthenticationListener<T extends BaseUserProfile> implements ApplicationListener<InteractiveAuthenticationSuccessEvent> {
42+
43+
/** The session profile manager for storing user profile data. */
44+
private final BaseSessionProfile<T> sessionProfile;
45+
46+
/** The service for retrieving or creating user profiles. */
47+
private final UserProfileService<T> profileService;
48+
49+
/**
50+
* Constructs a new BaseAuthenticationListener with the specified session profile and profile service.
51+
*
52+
* @param sessionProfile the session-scoped profile manager
53+
* @param profileService the service for managing user profiles
54+
* @throws IllegalArgumentException if either parameter is null
55+
*/
56+
protected BaseAuthenticationListener(BaseSessionProfile<T> sessionProfile, UserProfileService<T> profileService) {
57+
if (sessionProfile == null || profileService == null) {
58+
throw new IllegalArgumentException("Session profile and profile service must not be null");
59+
}
60+
this.sessionProfile = sessionProfile;
61+
this.profileService = profileService;
62+
}
63+
64+
/**
65+
* Handles successful authentication events by loading or creating the user's profile and storing it in the session.
66+
*
67+
* <p>
68+
* This method is automatically called by Spring's event system when a user successfully authenticates. It checks if the authentication principal
69+
* is a {@link DSUserDetails} instance, and if so, retrieves or creates the associated profile and stores it in the session.
70+
* </p>
71+
*
72+
* @param event the authentication success event
73+
* @throws IllegalStateException if the authentication details are invalid or missing
74+
*/
75+
@Override
76+
public void onApplicationEvent(InteractiveAuthenticationSuccessEvent event) {
77+
if (event.getAuthentication().getPrincipal() instanceof DSUserDetails) {
78+
DSUserDetails userDetails = (DSUserDetails) event.getAuthentication().getPrincipal();
79+
T profile = profileService.getOrCreateProfile(userDetails.getUser());
80+
sessionProfile.setUserProfile(profile);
81+
}
82+
}
83+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package com.digitalsanctuary.spring.user.profile.session;
2+
3+
import java.io.Serializable;
4+
import java.time.LocalDateTime;
5+
import org.springframework.context.annotation.Scope;
6+
import org.springframework.context.annotation.ScopedProxyMode;
7+
import org.springframework.stereotype.Component;
8+
import org.springframework.web.context.WebApplicationContext;
9+
import com.digitalsanctuary.spring.user.persistence.model.User;
10+
import com.digitalsanctuary.spring.user.profile.BaseUserProfile;
11+
import lombok.Data;
12+
13+
/**
14+
* Base class for session-scoped user profile management. This class provides the foundation for maintaining user profile data within the session
15+
* context of a web application. It is designed to be extended by applications to add custom profile management functionality.
16+
*
17+
* <p>
18+
* This class is session-scoped and uses proxy mode TARGET_CLASS to ensure proper session management in a web environment. It maintains a reference to
19+
* the user's profile and tracks when it was last updated.
20+
* </p>
21+
*
22+
* <p>
23+
* Example usage:
24+
* </p>
25+
*
26+
* <pre>
27+
* {@code
28+
* @Component
29+
* public class CustomSessionProfile extends BaseSessionProfile<CustomUserProfile> {
30+
* // Add custom methods for your application
31+
* public boolean hasSpecificPermission() {
32+
* return getUserProfile().getPermissions().contains("SPECIFIC_PERMISSION");
33+
* }
34+
* }
35+
* }</pre>
36+
*
37+
* @param <T> the type of user profile, must extend BaseUserProfile
38+
*
39+
* @see BaseUserProfile
40+
* @see WebApplicationContext
41+
* @see Serializable
42+
*/
43+
@Data
44+
@Component
45+
@Scope(value = WebApplicationContext.SCOPE_SESSION, proxyMode = ScopedProxyMode.TARGET_CLASS)
46+
public abstract class BaseSessionProfile<T extends BaseUserProfile> implements Serializable {
47+
48+
/** Serialization version ID. */
49+
private static final long serialVersionUID = 1L;
50+
51+
/** The current user's profile. */
52+
private T userProfile;
53+
54+
/** Timestamp of when the profile was last updated. */
55+
private LocalDateTime lastUpdated;
56+
57+
/**
58+
* Retrieves the current user's profile.
59+
*
60+
* @return the user profile of type T, or null if no profile is set
61+
*/
62+
public T getUserProfile() {
63+
return userProfile;
64+
}
65+
66+
/**
67+
* Sets the user's profile and updates the lastUpdated timestamp. This method is typically called during authentication or when the profile data
68+
* is modified.
69+
*
70+
* @param userProfile the user profile to set
71+
*/
72+
public void setUserProfile(T userProfile) {
73+
this.userProfile = userProfile;
74+
this.lastUpdated = LocalDateTime.now();
75+
}
76+
77+
/**
78+
* Convenience method to get the core User entity associated with the profile.
79+
*
80+
* @return the User entity if a profile is set, null otherwise
81+
* @see User
82+
*/
83+
public User getUser() {
84+
return userProfile != null ? userProfile.getUser() : null;
85+
}
86+
}

0 commit comments

Comments
 (0)