Pages

Showing posts with label MDS. Show all posts
Showing posts with label MDS. Show all posts

The Struggles of Personalization in ADF

A Mike Heeren & Richard Olrichs co-production

ADF comes with the out-of-the-box features of personalization. This means that whenever you configure personalisation, users can persist changes they make to the application across sessions and personalize their experience with the application. We have seen that this feature can also confuse some of our users, so it is not always wise to use this. It depends on the use case you have. However, when recently implementing personalization on an ADF 12.2.1.2 application, we had a couple of issues regarding persisting these personalizations to the MDS.

We felt that most of the blogs we came across while implementing these features, share the joyful out-of-the-box configuration. Just select some of the checkboxes in the properties and you are done, ready to enjoy your beer and have your designers and product owners cheer for you.
Sometimes however, real life applications at customers do not match the out-of-the-box configuration and things can become a little bit more tricky than you might expect.

Let's start at the top, and go through some of the steps you will always need within your application for personalization to work. You need to have authentication and authorization set up. If you also want to follow our struggles, we have posted a sample application at the end of this blog to show both the problems as well as the solutions.

Getting started with customizations

The basic configuration of customization in ADF is pretty simple. We start with a simple ADF application (with authentication already configured), and select the Project Properties > ADF View. Here we check Enable user customization and Across sessions using MDS, as seen below:



When enabling user customizations, the following files are edited:
In the adf-config.xml file the following lines are added:

 
  oracle.adf.view.rich.change.MDSDocumentChangeManager
 


In the web.xml file the javax.faces.FACELETS_RESOURCE_RESOLVER context-param is changed from oracle.adfinternal.view.faces.facelets.rich.AdfFaceletsResourceResolver to oracle.adfinternal.view.faces.facelets.rich.MDSFaceletsResourceResolver, and the following blocks are added:

 adflibResources
 oracle.adf.library.webapp.ResourceServlet

...

 adflibResources
 /adflib/*

...

 ADFLibraryFilter
 oracle.adf.library.webapp.LibraryFilter

...

 ADFLibraryFilter
 /*
 FORWARD
 REQUEST

...

 oracle.adf.jsp.provider.0
 oracle.mds.jsp.MDSJSPProviderHelper


 org.apache.myfaces.trinidad.CHANGE_PERSISTENCE
 oracle.adf.view.rich.change.FilteredPersistenceChangeManager


Finally, the following block will be added to the project .jpr file:

 
 


After this we need to configure the adf-config.xml file and add the oracle.adf.share.config.UserCC Customization Class on the MDS tab. This is a default customization class that is shipped with ADF. You can also write your own, but that is more of a use case for customization than it is for personalization. In our case we’ll just configure the application using the UserCC:



Now you need to configure the components that you want the end user to be able to personalize. This can be done in the View tab. Select the ADF Faces Components Tag Library, because we want to personalize the table and column components, these are default component from ADF Faces. By default all attributes will be persisted, you can uncheck them if you do not wish to persist these:



After the above steps are configured, you can give a fancy demo on your demo application, and everybody is happy. However, in our production application, there were still a few issues to tackle, we struggled with some of these.

Struggle 1: Task flows from libraries combined with a file based MDS on a Windows machine.


Our application was not a simple MVC application, but like many application out there, we used ADF libraries to include taskflows from library projects and included them in a bigger main application. When we turned on personalisation on components from task flows which come from libraries instead of directly from the application, they were not correctly persisted to the file based MDS on Windows machines.

When running the application via JDeveloper (in our example application by right clicking default.jsf in the ViewController project, and selecting Run), we see that the task flow that comes directly from the ViewController project (the table on the left of the screen), behaves as expected. However, when personalizing the table from the task flow that comes from the imported library (so from the ViewControllerLibrary project), we see the following warning in the log files.

    

We can verify that the preferences were persisted to the MDS for the left table, but not for the right table, by opening the application (with the same user) in another browser:



Unfortunately, this issue occurs when using task flows from libraries in combination with using a file based MDS on a Windows machine. Weblogic is not able to create a file path, which contains !/ (which is used to indicate that the resource is part of a library).

Luckily, in our case all other DTAP environments don’t use a file based MDS, but a database MDS. In the database MDS, we don’t have the file path issues, so there we won’t see this issue. So on all environments except the (local) Integrated WLS, we don’t see these warning logs, and we will see both table personalizations being persisted. It might take you some time to realise this, if you do not want to deploy to Dev or Test without having the Personalization working on your local machine.

Struggle 2: Deploying the application as EAR instead of via JDeveloper


Deploying both from JDeveloper as well as creating an EAR file seems to work. However, there was still some struggle there as well. The personalisation was working when deploying via JDeveloper, but when we would build in EAR from the application via JDeveloper, and deploy it manually to the Integrated WLS via the console, none of the settings were persisted to the MDS, and we saw the following warning in the log files:

     

By opening the application in different browsers again, we can also confirm that neither of customizations the tables is persisted in the MDS now:



When Personalization across sessions with the MDS is configured, JDeveloper always creates a metadata store usages in the adf-config file at deployment time. This configuration is named MAR_TargetRepos.

It does not matter that we have configured a different metadata store usages within the adf-config. If the MAR_TargetRepos is not present while creating the EAR file, it will be added to the adf-config file. The only solution that we found to this, is by naming our metadata store usages to match the expected default, then it will not override or add anything.


      
            
      
 
  
   
    
    
   
  
 


Note that we did not set deploy-target to true, because if we do this, our custom MAR_TargetRepos will be overridden with the default implementation again, when building the EAR file. This default implementation does not contain the FileMetadataStore implementation:



Because we want to override the default MAR_TargetRepos, so the personalization will also work when deploying the EAR instead of deploying via JDeveloper, we do not set the deploy-target to true so the default (false) will be used. In this case the FileMetadataStore configuration will be preserved in the EAR file.

If we deploy the new EAR file, we see the same behaviour as we did when deploying it via JDeveloper. Also, when we use the personalization functions on the screen, we will see files being created within the PersDef folder within the %TEMP% environment variable.

This FileMetadataStore configuration is changed (back) to a DBMetaDataStore configuration by an ANT build script, before deploying on the different DTAP environments instead of the Integrated WLS environment. An example of such ANT script can be found below.


  
  
  
  
 
  
  
  
  
  
 
 
   
   
 
 
  


Be sure that the WLS_HOME environment variable is set in the system environment variables, and the ear.location property is replaced in the ANT file when you want to use the above example.

Struggle 3: Suddenly our application does persisting during the session.


We have configured the adf-config to persist only certain components and attributes across the session. This works very nice and clear, however, suddenly all the other components also persist their state, just not across the session, but during the session.

It is possible that this is not what you want, it certainly was not what we expected or had in mind for our application, but there is nothing much we can do about it. It would have made more sense to turn this off for all the components and only persist those that were configured to be persisted.

Luckily the ADF components have an attribute persist and dontPersist on them. When reading the documentation on these attributes, it sounds exactly like what we need for our application! Before adding the dontPersist attribute to the hundreds of components we have in the application, we decide to test it on a couple. What we found out was very unpleasing, basically these attributes could be used for documentation purpose or for fun, but it certainly did nothing concerning persistence.

We decided to create our own custom class to adjust the framework and get this working. To achieve this, the context-param org.apache.myfaces.trinidad.CHANGE_PERSISTENCE can be adjusted to a custom class.

At first we tried to extend the oracle.adf.view.rich.change.FilteredPersistenceChangeManager class, which is the class ADF uses by default. However, unfortunately this class is declared final. We decided to create a class that extends the org.apache.myfaces.trinidad.change.SessionChangeManager class, and use the FilteredPersistenceChangeManager as an instance variable. This may not be the prettiest solution, but it serves our purpose:
package nl.whitehorses.personalization.changemanager;
import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;

import oracle.adf.view.rich.change.FilteredPersistenceChangeManager;

import org.apache.myfaces.trinidad.change.AttributeComponentChange;
import org.apache.myfaces.trinidad.change.ChangeManager;
import org.apache.myfaces.trinidad.change.ComponentChange;
import org.apache.myfaces.trinidad.change.DocumentChange;
import org.apache.myfaces.trinidad.change.SessionChangeManager;

public class CustomChangeManager extends SessionChangeManager {
 private final FilteredPersistenceChangeManager fpcmInstance = new FilteredPersistenceChangeManager();

 @Override
 public void addComponentChange(final FacesContext context, final UIComponent component, final ComponentChange change) {
     if (component == null || component.getAttributes() == null) {
         return;
     }
     final String[] persistArray = (String[]) component.getAttributes().get("persist");
     if (persistArray == null) {
         return;
     }
     for (final String persistVal : persistArray) {
         if (persistVal != null && change instanceof AttributeComponentChange && ("ALL".equals(persistVal) || ((AttributeComponentChange) change).getAttributeName().equals(persistVal))) {
             fpcmInstance.addComponentChange(context, component, change);
         }
     }     
 }

 @Override
 public void addDocumentChange(final FacesContext context, final UIComponent component, final DocumentChange change) {
     fpcmInstance.addDocumentChange(context, component, change);
 }

 @Override
 public boolean supportsDocumentPersistence(final FacesContext context) {
     return fpcmInstance.supportsDocumentPersistence(context);
 }

 @Override
 public ChangeManager.ChangeOutcome addDocumentChangeWithOutcome(final FacesContext context, final UIComponent component, final DocumentChange change) {
     return fpcmInstance.addDocumentChangeWithOutcome(context, component, change);
 }
}

As you can see, the logic for the persist attribute has been implemented in the addComponentChange method. The addComponentChange method from the FilteredPersistenceChangeManager instance, will only be called when the component contains the persist attribute which is set to ALL. Besides the addComponentChange method, all other (public) methods from the FilteredPersistenceChangeManager have been implemented to use the instance variable as well.
Conclusion

After some struggles and adjustments to the implementation and configuration of the application, we got personalization to work in our real world application used by customers. We overcame the struggles, but this was not as easy as the blogs on the internet made us believe beforehand. We hope that sharing this experience, might save you for some of the troubles we had.

To give you some more insight in the code and the struggles, we have created a (simple) demo application AdfPersonalization, to reproduce the issues we had, and which we used to solve them.

This application consists of the default Model and ViewController projects. Also, we added a ViewControllerLibrary project. This ViewControllerLibrary project is imported as a library by the ViewController project.

The AdfPersonalization application can be deployed to Weblogic in multiple ways:

  • Using the ‘Run’ button in JDeveloper.
  • Using Application > Deploy > … to IntegratedWebLogicServer in JDeveloper. This can be done to verify that struggle 2 is no longer an issue.
  • Using Application > Deploy > … to EAR, followed by running the replace-persdef-repo ANT target from the build.xml file. Afterwards the EAR can be deployed to a Weblogic servers, which is capable using a database based MDS. This can be done to verify struggle 1 is no longer an issue.
  • The source of this project can be downloaded via AdfPersonalization.zip.

Resources




Access your MDS-SOA repository from your ADF application

In our Enterprise Landscape the SOA Services and our Canonical Data Model (CDM) live on a different WebLogic Server than our ADF application. But often we want to use these services and CDM in our ADF application. 
Calling WebServices from an ADF application is not that difficult, however, we often want to manipulate the result that comes from this WebService. In 90 percent of the time, this are little adjustments to the existing CDM that are UI specific. This knowledge is not something we want to put in our CDM or in our WebService, but in the ADF application. We decided to create an UI specific XSD and map the result from the WebService in CDM to this UI specific XSD. 
To accomplish this, we created an XSD that extends the existing XSD from the CDM. The problem with this approach was to reach the XSD from the CDM that lives on the SOA Server from our ADF Application that lives on our ADF Server. 

It turns out, you can configure the MetaData Repositories on your WebLogic Server to look at different servers. When logged in to the Enterprise Manager, you can go to the MetaData Repositories, now if you click on an existing MDS, you can go through to menu to Administration -> Register/Deregister:


In here you see an overview of both the database and the file based repositories.
Now you can click the Register button to register a new database MDS repository.


This will bring up the screen to register a database based metadata repository in which you need to provide the information of your SOA MDS where the CDM is found.


After filling in the information, press query and enter the repository name ‘mds-soa’ and fill in the correct password. Pressing the OK button will register this mds-soa metadata repository and will bring up the next screen:


In here it is very important not to forget to target the correct server on which you ADF application is running.
Once this has been set up, your ADF application will be able to connect on run time to your CDM that lives inside the soa-mds repository under the /soa-infra partition. Next you need to configure your ADF application to actually look for this repository. You do this in the adf-config.xml:


In the adf-config file you see the mds-config, here you tell the application to look for any mds repository starting with /apps to look in the ‘mstore-usage_2’ this mstore is defined below in the metadata-store-usage section.
You need to enter the partition-name, which is ‘soa-infra’, the metadata-path is used locally and points to an environment variable. When deploying this application from JDeveloper, the deployment wizard understand that the MDS needs to be configured and it brings up the Deployment Configuration wizard:


In this wizard we select the mds-soa repository and select the correct repository and the correct partition. After this your ADF application will be able to resolve oramds urls and you can import XSDs from the CDM. As a result your ADF application specific XSD will be able to understand an import like this:


Meaning you can extend any XSD type defined in the CDM and make it UI specific for your needs.

Don't persist UIComponent state, now with attributes!

In this blogpost I showed how to disable the persistence of a UIComponent, but it was needed to write Java code to check for the specific component and change. In this blog entry I will show a more generic way to do this, with the attribute tag.

On the showDetailItem we define an attribute with a name and value:

        
        
     

The Java code for the addComponentChange method in the PortalComposerChangeManager now looks like this:
  public void addComponentChange(FacesContext context, UIComponent component,
                                 ComponentChange change)
  {
    Map attrs = component.getAttributes();
    String value = (String) attrs.get(NON_PERSIST_STRING);

    if (value != null)
    {
      logger.fine("NonPersist attribute found, we don't persist!");
      return;
    }
    super.addComponentChange(context, component, change);
  }

I defined a static final NON_PERSIST_STRING with the value "nonPersist", like the name of the attribute. If the attribute is found, we don't persist the componentChange.
If you want a more specific approach, you can still check for a specific ComponentChange, in the case of this example, we still check if the ComponentChange matches the value in the attribute (disclosed):
  public void addComponentChange(FacesContext context, UIComponent component,
                                  ComponentChange change)
  {
    Map attrs = component.getAttributes();
    String value = (String) attrs.get(NON_PERSIST_STRING);

    if (value != null)
    {
      if (change instanceof AttributeComponentChange &&
          value.equals(((AttributeComponentChange) change).getAttributeName()))
      { //we check if the attribute value equals the change.
        logger.fine("{0} attribute found; {1} event on {2}, we don’t persist.", new Object[]
            { NON_PERSIST_STRING, value, component.getClass() });
        return;
      }
    }
    super.addComponentChange(context, component, change);
  }

This code can be expanded or changed to check for specific changes according to your needs.
I uploaded a new workspace here, so you can download and browse the source if you want to.

Don’t persist UIComponent state in the session


Our use case was as followed: In our WebCenter Portal application we have a tableform layout, with a panelAccordion on the form layout. For other functionality we needed MDS persistence to be on, so we couldn’t disable MDS customizations. A side effect was that the panelAccordion remembered the showDetailItem that was disclosed and reopened the form page on the previously disclosed tab. 

We have the Seeded Customizations enabled, you can do this through the project properties:
 This results in a web.xml entry:
  
    org.apache.myfaces.trinidad.CHANGE_PERSISTENCE
    oracle.adf.view.page.editor.change.ComposerChangeManager
  
Now we can configure our adf-config file to exclude Persist Changes:

However, this doesn’t help you for your current session. We can see in the JavaDoc from the FilteredPersistenceChangeManager, the following documentation:
“If the DocumentChange fails through the restriction, the corresponding ComponentChange is persisted to the Session.”

We also don’t want to persist the changes in the session, so we had to subclass the ComposerChangeManager and override the addComponentChange method:
public class PortalComposerChangeManager extends ComposerChangeManager
{
  /**
   * adf-config.xml still persist in session, @see FilteredPersistenceChangeManager
   * We don't want this for the showDetailItem in the RichPanaelAccordion.
   *
   * @param context the FacesContext.
   * @param component the UIComponent.
   * @param change the ComponentChange.
   */
  @Override
  public void addComponentChange(FacesContext context, UIComponent component,
                                 ComponentChange change)
  {
    if (component instanceof RichShowDetailItem &&
        ((RichShowDetailItem) component).getParent() instanceof RichPanelAccordion &&
        change instanceof AttributeComponentChange &&
        "disclosed".equals(((AttributeComponentChange) change).getAttributeName()))
    {
      logger.fine("ComponentChange; disclosed event on showDetailItem in a RichPanelAccordion, we don’t persist.");
      return;
    }
    super.addComponentChange(context, component, change);
  }
}

This method is called by the framework to persist the component change. We override this method and  check if the component is a RichShowDetailItem in a RichPanelAccordion. If the ComponentChange is also a disclosed event, we don't call the super, so this change doesn't persist.

Since I don’t have my Oracle Cloud Trial anymore, I included a workspace, which reproduces this behavior. 
You can see the difference between the two accordions by changing the web.xml parameter. State is saved in session with the default web.xml param:

    Change Persistence Parameter
    org.apache.myfaces.trinidad.CHANGE_PERSISTENCE
    oracle.adf.view.page.editor.change.ComposerChangeManager
  

You can see the panelAccordion resetting to the first showDetailItem when you change to this parameter: 

    Change Persistence Parameter
    org.apache.myfaces.trinidad.CHANGE_PERSISTENCE
    nl.olrichs.blog.panelmds.portal.view.change.PortalComposerChangeManager
  


All you need to do for reproducing is:
  • Click the button toPanelAccordion.
  • Open a different showDetailItem.
  • Click the back button.
  • Click the button toPanelAccordion again.