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:
<adf-faces-config xmlns="http://xmlns.oracle.com/adf/faces/config">
<persistent-change-manager>
<persistent-change-manager-class>oracle.adf.view.rich.change.MDSDocumentChangeManager</persistent-change-manager-class>
</persistent-change-manager>
</adf-faces-config>
- 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:
<servlet>
<servlet-name>adflibResources</servlet-name>
<servlet-class>oracle.adf.library.webapp.ResourceServlet</servlet-class>
</servlet>
...
<servlet-mapping>
<servlet-name>adflibResources</servlet-name>
<url-pattern>/adflib/*</url-pattern>
</servlet-mapping>
...
<filter>
<filter-name>ADFLibraryFilter</filter-name>
<filter-class>oracle.adf.library.webapp.LibraryFilter</filter-class>
</filter>
...
<filter-mapping>
<filter-name>ADFLibraryFilter</filter-name>
<url-pattern>/*</url-pattern>
<dispatcher>FORWARD</dispatcher>
<dispatcher>REQUEST</dispatcher>
</filter-mapping>
...
<context-param>
<param-name>oracle.adf.jsp.provider.0</param-name>
<param-value>oracle.mds.jsp.MDSJSPProviderHelper</param-value>
</context-param>
<context-param>
<param-name>org.apache.myfaces.trinidad.CHANGE_PERSISTENCE</param-name>
<param-value>oracle.adf.view.rich.change.FilteredPersistenceChangeManager</param-value>
</context-param>
- Finally, the following block will be added to the project .jpr file:
<hash n="oracle.adfdtinternal.view.rich.setting.ADFViewSettings">
<value n="ENABLE_ADF_WEBAPP_LIB_SUPPORT" v="true"/>
<value n="KEY_ENABLE_USER_CUSTOMIZATIONS" v="2"/>
</hash>
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.
<Apr 5, 2018, 10:44:26,157 AM CEST> <Warning> <oracle.adf.view.rich.change.MDSDocumentChangeManager> <BEA-000000> <Attempt to persist a DocumentChange failed : MDS-02401: The operation ModifyAttribute on the column node is not allowed.
oracle.mds.exception.MDSRuntimeException: java.net.MalformedURLException: no !/ in spec
java.lang.NullPointerException: no !/ in spec>
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:
<Apr 5, 2018, 2:58:12,545 PM CEST> <Warning> <oracle.adf.view.rich.change.DocumentUtils> <BEA-000000> <ADFv: Trouble getting the mutable document from MDS. MDS-01273: The operation on the resource /WEB-INF/view-from-library.jsff.xml failed because source metadata store mapped to the namespace / DEFAULT is read only..>
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.
We added the following snippet to the adf-config/adf-mds-config/mds-config tag in adf-config.xml:
<persistence-config>
<metadata-namespaces>
<namespace path="/persdef" metadata-store-usage="MAR_TargetRepos"/>
</metadata-namespaces>
<metadata-store-usages>
<metadata-store-usage id="MAR_TargetRepos" default-cust-store="true">
<metadata-store class-name="oracle.mds.persistence.stores.file.FileMetadataStore">
<property name="metadata-path" value="${TEMP}"/>
<property name="partition-name" value="PersDef"/>
</metadata-store>
</metadata-store-usage>
</metadata-store-usages>
</persistence-config>
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:
<metadata-store-usage id="MAR_TargetRepos" deploy-target="true" default-cust-store="true"/>
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.
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="antlib:org.apache.tools.ant">
<property environment="env"/>
<property name="ear.location" location="/path/to/your/deployment.ear"/>
<!-- Classpath -->
<path id="wlst.classpath">
<fileset file="${env.WLS_HOME}/modules/features/wlst.wls.classpath.jar"/>
</path>
<!-- WLST task definition -->
<taskdef name="wlst" classname="weblogic.ant.taskdefs.management.WLSTTask" classpathref="wlst.classpath"/>
<!-- Target to replace persDef repository -->
<target name="replace-persdef-repo">
<echo message="Updating MDS config for [${ear.location}]..."/>
<wlst failonerror="true" debug="false" classpathref="wlst.classpath">
<arg file="${ear.location}"/>
archive = getMDSArchiveConfig(fromLocation = sys.argv[0])
archive.setAppSharedMetadataRepository(
namespace='/persdef',
repository='mds-owsm',
partition='PersDef',
type='DB',
jndi='jdbc/mds/owsm'
)
archive.save()
</wlst>
<echo message="Updating MDS config done!"/>
</target>
</project>
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-paramorg.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 ViewControllerLibraryproject 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.
Geen reacties
Geef jouw mening
Reactie plaatsenReactie toevoegen