Wednesday, May 23, 2012

Solution for Sharing Global User Data in ADF BC

This post will describe solution how to share custom user data in ADF BC. There is a way to access HTTP session scope directly from ADF BC - Bad Practice for Session Scope Access in ADF BC, while you can follow this approach in exceptional cases, this is not recommended from enterprise architecture point of view. Mainly because you may face snow ball effect - there will be more and more ADF BC and ADF UI dependency introduced over time and at the end will make system not maintainable. Solution is generic and will cover following requirements:

1. Custom user data will be initialized for every AM instance automatically
2. Custom user data will be preserved between passivation/activation requests

Download sample application - UserDataADFBCSample.zip. Implemented solution is based on such architecture:


Where on the higher level we have user preferences variables stored in HTTP session. For each Data Control, where we want associated Application Module to be initialized with custom variables for User Data, is defined custom Data Control factory class. Data Control factory class is calling generic Application Module implementation class, where it invokes generic method to populate User Data. Every AM extends from generic Application Module implementation class - this means it gets User Data set. There are standard methods for passivation and activation overriden inside generic Application Module implementation class - this allows to preserve custom User Data between passivation/activation requests.

From developer perspective, in order to use this solution for specific AM - only one configuration thing is required - set custom Data Control factory class, the rest will be handled by implemented generic classes. Select Data Control usage from DataBindings.cpx:


Set Data Control factory class to our custom class:


I will describe now, what happens behind the scenes - how this solution works based on proposed architecture.

1. Make sure Model project is configured with Base Classes, specifically in this case with generic Application Module class (responsible for extended activation/passivation process and User Data initialization):


Generic Application Module class overrides ADF BC framework passivateState(...) method - its the place, where we store custom variables from User Data along with standard ADF BC data into PS_TXN table:


activateState(...) method is overriden as well - to initialize activated custom variables back into User Data:


Generic User Data initialization method is pretty simple in this example, it stores only one variable:


Every AM should extend from generic class. For test purpose, I have implemented custom method to print User Data entry - you will see that it will be preserved across passivation/activation requests thanks to the Application Module implementation generic class - CustomAMImpl:



2. There is custom Data Control factory class defined. This class substitutes Data Control class with our own custom class:


Custom Data Control class overrides only one method - setSessionCookie(...). This method is called by the framework only once, when Data Control gets initialized (Data Control may get initialized multiple times during the same session, if you are using isolated Task Flows (not recommended)). From setSessionCookie(...) method, we are getting instance of generic Application Module implementation class (by casting Data Provider) and initializing User Data with user preferences variable value stored in HTTP session scope. This is completely generic, because action is done through generic class without touching underlying AM for selected Data Control:


3. Time to test. You should test with AM pooling OFF - means will simulate stress test environment and force AM instance to passivate/activate on each request:


With jbo detail logging enabled, we can see that passivation request stores into PS_TXN table along with standard ADF BC data, our own custom User Data also (because overriden method in CustomAMImpl):


Tags for custom entry and other data is declared inside CustomAMImpl. There is limitation of max 10 entries for custom User Data passivation is set, you can increase it (its safer to control this, in case someone will start to enter hundreds of entries into User Data):


When fragment is loaded, based on our test - passivation/activation happens (AM pooling is OFF). With default implementation of User Data - values will be lost, but now CustomAMImpl takes care for this. Press Print Uset Data Entry to test if variable from User Data is retrieved correctly:


Variable data is printed:


4. Task Flow isolated mode. This solution works for Task Flow isolated mode:


When task flow is opened in isolated mode - setSessionCookie(...) method from Data Control class gets invoked again:


This mean, there is new fresh AM instance created of the same Application Module. But, because setSessionCookie(...) method is called - user data gets initialized for that new instance. User data can be initialized not only during login process, but during session lifetime as well.

We can test this by opening isolated task flow:


Print user data variable:


Data is printed successfully:


5. Test without passivation/activation for custom User Data. Comment out custom code for User Data passivation inside CustomAMImpl class:


There will be no custom data added into passivation package:


User data will be NULL, after passivation/activation request:


22 comments:

Xahar Hassan said...

I'm still trying to figure out what is the username and password for logging into the example app. Please help.

Andrejus Baranovskis said...

Hi,

Username/password is standard for my blog samples: redsam/welcome1. If you want, you can add your username in jazn-data.xml

Regards,
Andrejus

rakesh menon said...

Hi Andrejus,

I have a doubt. can we access the userData in the session of ApplicationModule say AM1 from ApplicationModule AM2?

Andrejus Baranovskis said...

Hi,

By design in ADF, User Data exists only in scope of one AM instance. Its why I outline solution architecture in this post, how to initialize User Data in multiple AM's.

Regards,
Andrejus

rakesh menon said...

Hi Andrejus,


I have an application module inside which i am validating user inside a method. i want to create a user session object for each user that log in to the system. all my user credentials and roles are stored in the database tables. I want to make an object of this.

so if i create it in one of the application module .. so is it not possible to use the values in other application modules? what is the alternate method for storing the user session object?

Andrejus Baranovskis said...

Hi,

I would suggest to return value from that custom AM method into ViewController HTTP session scope.

Then you can initialize each AM User Data through custom Data Control Factory class - as per this blog.

Regards,
Andrejus

Scott Vossen said...

Hey Andrejus,

I have a use case where I need to load the session data more than just the one time setSessionCookie() is called. In fact I would like to fetch this data and put it into the userData map each time the AM is activated to ensure he AM has up to date data.

The use case goes something like this:

1) AM is instantiated for the first time, creating the Data Control and thus calling setSessionCookie to load the session data into the userData map.

2) AM performs some tasks and is passivated, storing the userData as per his blog.

3) AM is reactivated to perform some further tasks, but now has out of date session data in it's userData map since setSessionCookie will not be called again.

How would you recommend approaching this use case?

Andrejus Baranovskis said...

Hi,

I believe you can use same solution from the blog, just extend it a bit - add login of updating userData into prepareSession() method in AM Impl class. Its a place, where I'm initializing userData each time during activation.

setSessionCookie is used to load default data. But from prepareSession() you can modify it during each activation if needed.

Regards,
Andrejus

Anonymous said...

There is one problem in method activateState, for read only first element from jbo.custom.userdata
It should be like this to read all elements in tag jbo.custom.userdata
if (parent != null) {
NodeList nl = parent.getElementsByTagName(USER_DATA);
if (nl != null) {
for (int i = 0; i < nl.getLength(); i++) {
NodeList childNodes = nl.item(i).getChildNodes();
for (int nodesIndex = 0; nodesIndex < childNodes.getLength(); nodesIndex++) {
Node node = childNodes.item(nodesIndex);
if (node != null && node.getAttributes() != null) {
NamedNodeMap attrs = node.getAttributes();
if (attrs.getLength() == 2) {
Node userDataElementKey = attrs.getNamedItem(USER_DATA_ELEMENT_KEY);
Node userDataElementValue = attrs.getNamedItem(USER_DATA_ELEMENT_VALUE);
if (userDataElementKey != null && userDataElementValue != null) {
String key = userDataElementKey.getNodeValue();
String value = userDataElementValue.getNodeValue();
this.getSession().getUserData().put(key, value);
}
}
}
}
}
}
}

Andrejus Baranovskis said...

Yes, thanks for update. I was aware of it, I think it was fixed in this post: http://andrejusb.blogspot.com/2012/08/solution-to-control-global.html

Andrejus

Anonymous said...

Hi Andrejus,

we have the use case that we need to access some session data in our AM. So we tried it this way. However we need those session data in the prepareSession method of the AM.
It seems that the prepareSession is already called when the AM is instantiated, and our data is not set in the AM at that time.
So what would be your approach to get the data in the prepareSession without violating the MVC pattern.

Andrejus Baranovskis said...

I would need to test this.

Andrejus

Anonymous said...

Hi Andrejus,

I have a doubt. We have multiple root AMs with nested and shared AMs. Inside the shared AM we would have to access the userData. We want to store global user data that will be accessed from view layer and ADF BC. What is the alternate method to store the user session object in this scenario? Use HTTP Session (breaks the MVC pattern?)

Thanks

Andrejus Baranovskis said...

Yes, you should not use Session scope directly in ADF BC, instead you could use approach described in this post - initialising ADF BC params through custom ADF Data Control class.

Regards,
Andrejus

Anonymous said...

Thanks Andrejus for your answer

If I understand well, you say that we may use the CustomADFBCDataControl class to initialize the application module. Inside this class we can use the sessionScope or the HTTP Session without break the MVC pattern?

Another question: If we have one root application module that uses a shared AM, the userData map only exists in the root AM? Then, when we do am.setUserDataEntry, what userData map is accessed? With nested AM occurs the same?

Thanks for your help

Andrejus Baranovskis said...

Hi,

1. Yes, this is the whole point of this post - demonstrating solution how to keep MVC

2. You should test it. But I would recommend not to use nested AM's at all. New ADF versions are nesting AM automatically for you. If you want to use less DB connections - better use DB pooling

Andrejus

Bharath said...

Hi Andrejus,
I am trying to implement this approach for sharing some global context values between ADF BC and HTTP Session. However, my context value is a serialized object and I am not able to deserialize it in the BC.I am seeing that the object is getting saved in the session properly (as an object) but when I try to retrieve in BC, it is coming as String. Any help on this is greatly appreciated.

Andrejus Baranovskis said...

I don't think this is possible. You need to passivate parameters and on activation recreate your object.

Andrejus

Bharath said...

Andrejus,
Do you mean that we cannot save a POJO in the BC session and it always has to be key-value pairs of String type?

Thanks,
Bharath

Andrejus Baranovskis said...

ADF BC passivation saves string values, POJO is a object. You will need to recreate POJO on activation from parameters.

Andrejus

bharathreddy danda said...

Hi,
Thanks for the input. Here's what I have done. I wrote 2 methods a) Object to Map b) Map to Object
In passivate, I am converting the POJO into a Map and then saving all the map entries as elements.
In activate, I am copying the data from the elements back to the Map and at the end recreating the POJO.
SO far I have not had any issues. Thanks for your help..!

Andrejus Baranovskis said...

Perfect!