Modifying the Application Module's JDBC DataSource at run time

In normal scenario, your Application Module(AM) may have a static JDBC data source configured and will be connected to the single data base in all conditions. However there are cases where the application may need to display data from different data bases based on user profile. In today's post, I talk about an example for the above use case. Well, this is not a new topic as such. In fact, this has been discussed previously many times.

1. Dynamic JDBC Credentials example by Steve Muench
2. How To Support Dynamic JDBC Credentials

I'm copying the same solution in a slightly modified form ;)

Programmatically setting DataSource for an Application Module

A bit of theory before get in to the implementation. When a Fusion Web Page is getting rendered, UI controls may try to get data by evaluating the expression set for the value attribute. At this stage binding layer comes in to action, and will identify the underlying data provider(your Application Module) for the the specific attribute. When the binding layer refers Application Module, run time would either an instance or check out one from the AM pool. By default, AM instance is configured using the static meta data from bc4j.xcfg file, which is generated at design time. The design time configuration can be very well overridden by system parameters supplied at run time (java -Dproperty=value).

In the above use case, the requirement is to override the default data source specified for an AM. Interestingly, ADF BC provides a mechanism to override the configuration properties dynamically using a custom EnvInfoProvider. This part is well explained in Fusion Developer's Guide - 41.2.4 How to Programmatically Set Configuration Properties.

In this example, I'm trying to make use of this custom EnvInfoProvider which may get engaged whenever run time needs configuration properties for an Application Module. You may also need to keep jbo.ampool.dynamicjdbccredentials=true(default is true) to enable additional pooling lifecycle events to allow developer-written code to change the db credential.

The custom EnvInfoProvider used in this example may look like as shown in the following picture. You can see that the DynamicEnvInfoProvider::getInfo(...) overrides the default configuration property for Configuration.JDBC_DS_NAME(JDBC DataSource) using value from a session variable.

 public class DynamicEnvInfoProvider implements EnvInfoProvider {  
  public Object getInfo(String infoType, Object env) {  
  if (EnvInfoProvider.INFO_TYPE_JDBC_PROPERTIES.equals(infoType)) {  
      Map session = ADFContext.getCurrent().getSessionScope();  
      Object dsName = session.get(Configuration.JDBC_DS_NAME);  
      if (dsName != null) {  
           if (((Hashtable)env).containsKey(Configuration.JDBC_DS_NAME)) {  
                ((Hashtable)env).put(Configuration.JDBC_DS_NAME,  
                                          (String)dsName);  
           }  
      }  
  }  
  return null;  
 }  

Note

When you enable dynamic credentials (jbo.ampool.dynamicjdbccredentials) the AM JDBC credentials will be updated whenever a new DataControl is constructed (new AM handle is created). The credentials will NOT be updated at the begin of each request in a stateful session, even though the AM is released at the end of each request.

Configuring a custom EnvInfoProvider for AM

There are two possible approaches for hooking your custom EnvInfoProvider with AM.

Approach 1 - Using custom session cookie factory class

a) Implement a custom session cookie factory class to set the custom EnvInfoProvider on the SessionCookie instance. Runtime will check for overridden EnvInfoProvider on the session cookie whenever JDBC data source needs to be created.

   @Override  
   public SessionCookie createSessionCookie(String applicationId,  
                        String sessionId,  
                        ApplicationPool pool,  
                        Properties properties) {  
     SessionCookie cookie =  
       super.createSessionCookie(applicationId, sessionId, pool,  
                    properties);  
     EnvInfoProvider provider = new DynamicEnvInfoProvider();  
     cookie.setEnvInfoProvider(provider);  
     return cookie;  
   }  
b)Configure AM to use the custom session cookie factory class


The following class diagram depicts the runtime engagement between the classes.


Approach 2 - Using Application Module configuration parameter - jbo.envinfoprovider

Configure 'jbo.envinfoprovider' to use custom EnvInfoProvider class using the AM configuration editor as shown in the following screen shot. Well, this seems to be more straight forward and much simpler compared to Approach 1.


The above action may update the bc4j.xcfg file as shown below.

Please note that, you may face some weird error at design time if you try modifying this property using JDeveloper 11.1.1.4.0 release. In case if the editor errors out while manipulating this property, then please follow the Approach 1(though you may need to add a couple of 'infrastructural' classes)

 <?xml version = '1.0' encoding = 'UTF-8'?>  
 <BC4JConfig version="11.1" xmlns="http://xmlns.oracle.com/bc4j/configuration">  
   <AppModuleConfigBag ApplicationName="model.AppModule">  
    <AppModuleConfig DeployPlatform="LOCAL" RELEASE_MODE="Stateless"  jbo.project="model.Model" name="AppModuleLocal" ApplicationName="model.AppModule">  
      <AM-Pooling jbo.doconnectionpooling="true"  />  
      <Database jbo.locking.mode="optimistic"/>  
      <Security AppModuleJndiName="model.AppModule"/>  
      <Custom JDBCDataSource="java:comp/env/jdbc/HRDS" jbo.envinfoprovider="binding.extension.DynamicEnvInfoProvider"/>  
    </AppModuleConfig>    
   </AppModuleConfigBag>  
 </BC4JConfig>  


Download

You can download the sample workspace from here.
[Runs with Oracle JDeveloper 11g R1 PS3 + HR Schema]

How to run this sample?

The 'CoreBindingRuntime' project in the example workspace contains custom implementations for EnvInfoProvider and HttpSessionCookieFactory classes. Take a look at this project to see the implementations.

1. To run this example you may need to configure a JDBC Data Source (pointing to HR schema) in your weblogic server. You can do this using weblogic admin console.

2. Run the entry.jspx, key in the JNDI name of the JDBC data source(refer step 1) and click on command button


3. You can see the data source what you keyed at step 2 is used by the next page to execute the view object.

Learn More ...
There are a lot more points like this. If  you are curious to learn the internals of the ADF Business Components and ADF Binding Layer,  the following book is for you - Oracle ADF Real World Developer’s Guide.
More details about this book can be found in this post- http://jobinesh.blogspot.in/2012/07/pre-order-your-copy-of-oracle-adf-real.html

Comments

  1. Hello, Jobinesh!
    I attempted to use your approach in my ADF Swing WebStart application.
    As a datasource, I use datasource(named hrGlobal), which is declared on my application server(Weblogic 10.3.3). When I run my Webstart application i get an error:
    (oracle.jbo.DMLException) JBO-27200: Failure JNDI. Can't execute search of datasource in context hrGlobal
    It is really use your method in ADF 11G Swing WebStart applications?
    Thank you for answer.
    Regerds, Stanislav

    ReplyDelete
  2. Stanislav,
    Swing application does not have any any JDBC data source definition. The above approach is valid where you have a container with JDBC data source. You may need to use JDBC connection URL as in Steve blog(you can find the link at the beginning of the blog post)

    ReplyDelete
  3. Hi Jobinesh!

    In my project, I have a model and 4 ViewController Project. The session scope is not maintained in my application.

    Scenario is as follows:

    I have a page where I have 2 regions which belongs to different View Controller Projects(Using InlineFrame).

    Can you tell me how to make the variable visible all through the application.

    Please give your suggestion.

    Thanks,
    Dinesh

    ReplyDelete
  4. Thanks for the experiment. It was very informative and useful. I keep in mind. Thanks a lot for sharing such a awe-some information.
    IPhone App Development| Android apps developer| Android development|

    ReplyDelete
  5. hi Jobinesh, i tried using "DynamicEnvInfoProvider". It works , but not 100%.

    The issue is, say i have DS1 and DS2 in the server.

    From UI if i switch from DS1 to DS2, still the BC returns data from DS1.

    But if i make the DS1 not available in the server, then it takes the DS2 and gets the data.

    I guess caching issues.. How make the fresh calls to back end every time.. or how to make each call from UI has to call the back end. Please suggest. Thanks.

    ReplyDelete
  6. Try setting jbo.doconnectionpooling = true and jbo.txn.
    disconnect_level = 1 for the corresponding AM

    ReplyDelete
  7. Well, this config is supposed to read for a user session, not across request. If that's the case, Yes its by design

    ReplyDelete
  8. Okay. For our scenario its okay to kill the session. Because the switching of DS is required when user chose different application which will fetch data from different DS.

    I'm using "DynamicEnvInfoProvider" from your sample code. Which place is right one to kill the existing session and create new session to bind the new DS.



    ReplyDelete
  9. Invalidating a session for a real life app means enable re-login for a user, which is supposed to be done from client tier/managed bean. Sorry I don't have any sample for it

    ReplyDelete
  10. Hi Jobinesh,

    In the How To Support Dynamic JDBC Credentials (http://www.oracle.com/technetwork/developer-tools/jdev/dynamicjdbchowto-101755.html#toc) article, the author overrided ADF Binding Filter, saying it would otherwise checkout an AM from pool which would not have DB connection.
    In your example, you have not done such overriding.

    When will an AM get requested exactly? Is it the first time when a page containing UI controls linked to DB starts rendering or when even any page in that application - not having DB based UI gets rendered?

    I assume the former is correct, based on your sample. Then what is the need to override ADF Binding Filter in the example of 'How To Support Dynamic JDBC Credentials'?

    Thanks
    Chinni

    ReplyDelete
  11. Chinni
    The EnvProvider used in this sample is read when system starts a new user session. I other words, you can use the approach in this post to supply new DB details for different users, not within a session. I don't think you really need to touch ADF Binding Filter for the same.

    ReplyDelete
  12. Thanks for the clarification. I was wondering why the ADF Binding Filter had to be in picture. I tried to superimpose that implementation on your sample and ended up loading the entry.jspx again and again continuously :-)

    btw, when does the AM get instantiated (and get a DB connection)? As soon as a session is created or only when a page containing UI elements based on datacontrols is first rendered?

    Does the ADF Binding Filter checkout an AM as soon as it creates a session even if the page doesnt really require DB access?

    For example, in this sample, loading the entry.jspx does not instantiate any AM, correct?

    ReplyDelete
  13. Well...It need a detailed answer...You can take a look at the Appnedix of my book which explains "Life-cycle of an ADF Fusion web page with region" (Appendix is free to download) http://www.packtpub.com/sites/default/files/downloads/4828EN_Appendix_More_on_ADF_Business_Components_and_Fusion_Page_Runtime.pdf

    There more stuffs explained in the respective chapters as well. http://www.packtpub.com/oracle-application-development-framework-real-world-developers-guide/book

    ReplyDelete
  14. Hi,

    I am creating login based on your sample and it works gr8 ,in my screen i have 3 fields userid,pwd,db.

    On button click i have code for validating user from db and it works fine ,now if its invalid user i throw message so thats fine.

    But now if i change datasource and click ,i dont see the effect of the data source change.

    Datasource doesnt seems to be getting refreshed.

    Can you help me on that?

    Regards,
    Sandy

    ReplyDelete
  15. It's associate with the user session and you cannot chnage data source once the session is activated and AM is checked in. You may need to try invalidating the session before changing the data source - a wild guess, I haven't tried it personally though

    ReplyDelete
  16. Thanks for the reply ..it worked !!!!,so on invalid user id case now showing poup message and on ok button i wrote...
    FacesContext ctx = FacesContext.getCurrentInstance();
    HttpSession hs = (HttpSession)ctx.getExternalContext().getSession(true);
    hs.invalidate();

    so when user logs in again then datasource is getting refreshed.

    Regards,
    Sandy

    ReplyDelete
  17. Hi Jobinesh,
    I have gone through this blog and it is very useful. Thanks for your regular posts.

    I have one doubt with this implementation of Dynamic Credentials. I see that we are setting the JDBCDatasource from session in the runtime at the start itself. By start itself I mean the first call to bindings/Application Module will be with the DataSource that we have set/entered.

    Now suppose I want to change the datasource again, how can i do it?
    I saw that the call to SessionCookieFactory happens only for the first call to bindings/references at the start of a session.

    So should i invalidate the session and start a new session so that it call the SessionCookieFactory class again and tries to set the DataSource? I tried this but it throws me the following exception.


    oracle.adf.controller.ControllerException: ADFC-14031: Failed to find a DCTaskFlowBinding with name 'data.COP_view_ConfigPageDef.configtfd1'.
    at oracle.adf.controller.internal.binding.TaskFlowRegionModelViewPort.getTaskFlowBinding(TaskFlowRegionModelViewPort.java:923)
    at oracle.adf.controller.internal.binding.TaskFlowRegionModelViewPort.isActive(TaskFlowRegionModelViewPort.java:984)
    at oracle.adf.controller.internal.binding.TaskFlowRegionModelViewPort.getViewId(TaskFlowRegionModelViewPort.java:559)
    at oracle.adf.controller.internal.binding.TaskFlowRegionModelLocal.getViewId(TaskFlowRegionModelLocal.java:12)
    at oracle.adf.view.rich.component.fragment.UIXRegion.broadcast(UIXRegion.java:185)
    at oracle.adf.view.rich.component.fragment.ContextSwitchingComponent$1.run(ContextSwitchingComponent.java:92)
    at oracle.adf.view.rich.component.fragment.ContextSwitchingComponent._processPhase(ContextSwitchingComponent.java:361)



    Since I have invalidated the session, its not able to find the taskflow binding. How can I avoid this exception and connect to the new Datasource that I have set into session.

    Regards,
    Srikanth

    ReplyDelete
  18. Hi Jobinesh,

    I am very new to ADF.I have the task like while selecting the datasource name(jdbc/HRDS) from UI as a af:selectOneChoice then corresponding ApplicationModuleDataControl name has been populated as another af:selectOneChoice , again select any ApplicationModuleDataControl name corresponding ViewObjects name must be shown in other af:selectOneChoice.
    I completed upto dynamically selecting the datasource name.
    After that I am strugling to get Corresponding ApplicationModuleDatacontrol name and ViewObject name.

    Thanks
    SaravananVasu

    ReplyDelete
  19. Hi Jobinesh,

    I am using Jdev 11.1.1.7.
    I have an ADF model project with AM connected to DB1. And in AMImpl we have a method called XYZ(), which initially queries data from table (TAB1) in DB1 (through VOs) and inserts data into a table (TBL2) of DB2 using insert statement (JDBC code).
    I created a deployment profile (ADF Library Jar file - JAR1) for this.

    Now I have another ADF application which is based on DB2. I have added JAR1 to the model project of this application and calling XYZ() from this application AMImpl.
    As context is created in 2nd application, JAR1 is getting executed in the context of 2nd application and i am getting table does not exist error (As table TAB1 is not present in DB2).

    How to execute the JAR1 in its own context when request is initiated from another application?


    Regards
    PavanKumar

    ReplyDelete

Post a Comment