Jasig/uPortal — pull request #450 by apetro (2014-10-30 18:18:09+00:00)
Title: UP-4223 Alternative Sticky Profiles implementation

Alternative implementation of #416 , implementing sticky profile selection.

---

Adds `StickyProfileMapperImpl` and supporting plug in points and JPA persistence.

See also

* [Blog post articulating the problem and solution design](http://apetro.ghost.io/sticky-profiles/)
* [uportal-dev@ list thread](http://thread.gmane.org/gmane.comp.java.jasig.uportal.devel/5911)
* JIRA issue [UP-4223][]

---

`StickyProfileMapperImpl` adds optional persistent stickiness of user selection of profile.

In implementing this:

* De-couples `/Login` signalling of runtime desired profile, from a tightly coupled Login-talks-directly-to-the-profile-mapper-that-cares to a loosely-coupled Login-fires-an-event-and-caring-components-listen-for-the-event.
* Refactors profile-related portions of the current `layout` package into a `profile` sub-package.
* Adds a couple `toString()`s and corrects a unit test naming glitch.

Sticky what? What feature does this add?
=========================================

(Profiles are bundles of XSLT transform and other configuration that makes the portal render the layout in different ways, e.g. responsively, or mobile-optimized, or traditionally).

Before this changeset, users could access

/Login?profile=bucky

and opt in to a non-default profile keyed by "bucky" *for the duration of one session*. On session expiration or next login, that selection is gone, though the user might again select a profile through request parameter. This is ad-hoc profile selection for a short "hoc".

With this changeset, *if so configured* (not the default configuration), users accessing

/Login?profile=bucky

will have that selection persisted and it will be re-applied on subsequent logins until they select a different profile. This is ad-hoc profile selection for an indefinitely long "hoc". Selecting the default has the special behavior of clearing the preference.

Database schema additions
======================

This changeset introduces a new JPA persistent object class, namely `ProfileSelection`. This adds to the uPortal database schema implied by that JPA usage. Adopting this feature requires realizing that schema change in your uPortal database.

```
ant db-hibernate-portal-update
```

will create a suitable `UP_PROFILE_SELECTION` table for you.

Note that this `ProfileSelection` persistent object is declared in the hibernate mappings XML file.

How to configure
================

If you do not want profile selection to be sticky across sessions, no need to update the configuration. :smile:

If you do want profile selection to be sticky across sessions, as MyUW does, add this profile mapper to your `userContext.xml`.

You probably want to add it *after* the existing Session scoped mapper. The first mapper to return a profile fname controls. So, with the new sticky mapper configured *after* the existing session mapper:

* if there's an ad-hoc profile selection for that session, the session-scoped mapper will return it.
* otherwise if there's a persisted profile selection (from some previous session), the sticky-scoped mapper will return it.

The Sticky mapper will return the profile stored from that very same session, so you might think you could replace rather than supplement your session-scoped mapper. And maybe you can! However, the sticky selector ignores selections in the context of impersonated user sessions. That's intended as a feature in that impersonating a user under a profile to troubleshoot something won't persist that selection for the user being impersonated. However, if you don't retain the session-scoped mapper, then when you try to impersonate a user using a particular profile this will not work since the Sticky mapper will not reflect the selection and without the session mapper nothing else will reflect that selection either. So you probably want both. And you probably want the session-scoped selector to be first in the order so that it has an opportunity to override any persisted selection the impersonated user might have that would be reflected by the Sticky mapper.

Here's the anticipated MyUW configuration for this feature:

```

...
```

How the solution is implemented
===============================

### Changes to `/Login` handling

`/Login` handling (implemented in `PortalPreAuthenticatedProcessingFilter`) was tightly coupled to the implementation of `SessionAttributeProfileMapperImpl` in that it would put the requested profile key into the session at the Well Known Location that it knew `SessionAttributeProfileMapperImpl` would check.

#437 would change this tight coupling. `/Login` handling would be still tightly coupled, but through `/Login` handling calling `SessionAttributeProfileMapperImpl` instead of making assumptions about what that other class wanted to do in the session with this information. (This turned out to be handy for adding some logging troubleshooting how profile selection was working, which was the purpose of #437 .)

This changeset removes that tight coupling, with `Login` handling instead firing a `ProfileSelectionEvent` through the Spring `ApplicationContext`. Profile mappers (or anything else for that matter) listen for the event (by implementing a suitable event handling interface). This means that profile mappers other than `SessionAttributeProfileMapperImpl` can react to runtime profile selection. `StickyProfileMapperImpl` ends up being such a listener.

This changeset also tweaks where in the `/Login` handling the event is fired, so that more is known about the user at the time it is fired. In particular, it's important to know *who* is doing the requesting, so that the sticky profile mapper knows with whom to associate the selection (whereas the merely session-scoped implementation could cope with anonymous-from-its-perspective sessions).

### ProfileSelectionEvent

The profile selection event conveys the user doing the profile requesting, the key indicating the desired profile, and the `HttpServletRequest` context for the request.

Including the `Request` in the event feels pretty hacky, but `/Login` handling is this special case where the metadata on the request is changing in flight such that it's troubling to rely upon other approaches (filter that shoves context into `ThreadLocal` most prominently) for event recipients to understand the context of the event other than from interrogating the `Event` itself.

### Changes to `SessionProfileMapperImpl`

`SessionProfileMapperImpl` listens for the `ProfileSelectionEvent` and stores the selection into the session, rather than relying upon another component to have set that session attribute for it to find.

### StickyProfileMapper

`StickyProfileMapperImpl` listens for the `ProfileSelectionEvent` and stores the selection into the database.

However, it *doesn't* store the selection into the database if the selection is in the context of an impersonated session, so that impersonating a user under a profile selected for impersonation doesn't update the impersonated user's profile selection.

**This storage is keyed by the username rather than by the user ID. That may be a mistake**, but losing profile selection on username change seems low consequence. Anything think this ought to be keyed by `IPerson` `getID()` rather than by username?

### Service-Registry-DAO-JPA Architecture

This implementation adheres to the uPortal [Controller-Service-Registry-DAO-JPA architecture][].

The `StickyProfileMapperImpl` relies upon an `IProfileSelectionRegistry`, the implementation of which relies upon an `IProfileSelectionDao`, the JPA implementation of which uses a JPA implementation of the `IProfileSelection` domain object.

The StickyProfileMapperImpl *is* the Service layer, so there is no `ProfileSelectionService` as such.

#### ProfileSelectionRegistry

This data really wants to be a simple key-value mapping of user identity to selected profile if any, so that's the API exposed by the Registry.

```
public interface IProfileSelectionRegistry {

public String profileSelectionForUser(String username);

public void registerUserProfileSelection(String userName, String profileFName);

}
```

#### IProfileSelectionDao

There's a DAO API providing CRUD operations over `IProfileSelection` instances.

#### IProfileSelectionDao JPA implementation

The only implementation of `IProfileSelectionDao` is the JPA implementation, of course.

Characteristics of the solution
================================

### Composable local configuration

This solution relies upon the power of `ChainingProfileMapperImpl` to chain together `IProfileMapper`s. Local implementations can choose whether profiles are sticky, and whether and what other profile mappings the sticky selection overrides, by manipulating the profile mapper chain configuration.

### Supports impersonation

This solution supports impersonation, which is useful for development and troubleshooting, by reflecting the previously-stuck profile selection of the impersonated user but not writing new sticky configuration during impersonated sessions.

### Unit tested

Unit tests cover the changes.

Refactor mercilessly
====================

I hope this changeset is clean and is made up of logical, atomic commits that are documenting.

If it is, that's only through heroic interactive rebasing and merciless refactoring. :smile: . The [actual development of this solution was messy](https://github.com/apetro/uPortal/tree/UP-4223-ap2).

False starts thought better of:

* This solution relies upon a Spring mediated `ProfileSelectionEvent` (whereas I'd first gone down a road of introducing `IProfileSelectionRequestHandler` and enhancing `ChainingSecurityContextImpl` to broadcast). So glad to have been reminded of Spring `ApplicationEvent` support.
* I'd gone down a wrong path of implementing an `IProfileSelectionService` service layer before realizing that `IProfileMapper` *is* the service layer.

This solution re-packages the profile mappers. That's going to inflict a little pain in that adopter `userContext.xml` files will need tweaked to reflect the new package path. I think that refactor is worth it to get to cleaner, smaller, tighter cohesive packages separating profile mapping from the many other things `...layout` does.

[UP-4223]: https://issues.jasig.org/browse/UP-4223
[Controller-Service-Registry-DAO-JPA architecture]: https://wiki.jasig.org/display/UPC/POJOs+DAOs+Registries+and+Services