Monday, 25 January 2010

JBO-34010 + JBO-25058: Master ADF applications, Bounded Task Flow sub-applications and ADF Libraries

(JDev 11g 11.1.1.2.0)

Under JDev 11g when you start playing with importing Bounded Task Flows (BTF) through ADF Libraries into a master ADF application, you're likely to start encountering the errors JBO-34010 and JBO-25058. This post discusses why these errors occur in context of ADF Libraries, and composite master ADF application comprised of 1 or more BTF sub-applications.

JBO-34010

JBO-34010: The "view/DataBindings.cpx" descriptor appears in the application classpath more than once

This error can occur because the ADF runtime demands that if an application includes multiple DataBindings.cpx files they must live in different Java packages. We've hit this when we've created a single master ADF application that loads one or more Bounded Task Flow sub-applications via the ADF Libraries feature, where each application has dropped the created DataBindings.cpx file in the default ViewController package "view".

You have two options in solving this issue:

The first option is to avoid the issue in the first place: when creating your ADF applications, be they a master application or BTF application, ensure to change the default package of the ViewController project via the Project Properties -> Project Source Paths -> Default Package option, or when you create the application change the package options in the Create Application wizard.

The second option is to make a retrospective change by selecting the view package in the ViewController project of each of your applications and refactor the package name. Be warned in the JDev 11g PS1 build 5536 I've had varying degrees of success with this option (read: lots of failures). Sometimes the refactor option messes up the new package name, other times JDev refuses to forgot the old view/DataBindings.cpx files until you Clean All under the Build menu on all the applications. Sometimes I have to carefully hand craft the fix by dropping out of JDeveloper and changing the files manually. I'm probably doing something wrong here, but it seems a bit of a mess. Best go for the proactive solution to save heartache.

JBO-25058

(For readers Googling in on this post/error code, please note this error can occur for other reasons beyond the scenario described in this post. I refer you to Andrejus Baranovskis's post and otherwise undertaking a careful search for further useful resources.)

oracle.jbo.NoDefException: JBO-25058: Definition EventNo of type Attribute is not found in EventNo.

Note EventNo is specific to my demo application, you'll have other attribute names here.

This error can occur where within each application, they refer to the same Application Module Data Control name, typically AppModuleDataControl created by default by JDeveloper. This name must be unique across DataBindings.cpx files, which can be located in your master application and separate BTF applications.

The first option is to avoid this issue in the first place: the Data Control name when created in the DataBindings.cpx file by default uses the Model project's Application Module name with suffix DataControl. Thus if your AM is called AppModule, the default name for the Data Control is AppModuleDataControl. Instead when creating your AppModules in an ADF BC Model project you give them a unique name, say ProcurementAppModule or EnrolmentServices, those names will be carried to the creation of the DataBindings.cpx as ProcurementAppModuleDataControl and EntrolmentServiceDataControl respectively. As such collisions in the Data Control names are less likely to occur.

The second option is to make a retrospective change by changing the Data Control name in each DataBindings.cpx file. However be warned that you must also update any pageDef file to then refer the new Data Control name too.


With regards to both issues above, as there are proactive solutions to avoid the problems in the first place, by ensuring a unique ViewController package name and a unique Application Module name in the Model project, it's recommended these become coding standards adopted by your organisation.

Wednesday, 13 January 2010

ADF Fail Whale: Handling the database is down gracefully

(JDev 11g 11.1.1.2.0)

Like most Oracle applications, when an ADF application loses its connection to the database, the games up. There's really not much you can do. ADF does detect this situation, presenting the following popup to the user:


But to most users a JDBC error would mean nothing, particularly if your application is delivered to the general public on the internet. In turn the user is left in the application without the ability to do much, resulting actions showing the same popup JDBC error again.

Ideally what we'd like to do is redirect to a web page that gives more useful information, maybe something like the famous Twitter Fail Whale:


The following blog post shows you a solution to do just this. This solution is based on Steve Muench's Dynamic JDBC Credentials example #129. In addition I must give my thanks to Oracle Support for pointing me to Steve's solution.

As usual, please note this solution has yet to be proven in a production environment. Seriously, I've run some arbitrary tests to see if the technique works, but no idea if it'll cover all situations where the database goes down. It's important if you take this example that you test it to ensure it meets your own needs. I'd appreciate it if anybody who does find any issues and resulting solutions, if you could please post them on this post as a comment to assist other readers.

With the following solution I'm not going to bother to explain all the moving parts, just give you the code and where it goes. I'll leave the reader to follow up with their own research on the mechanics of this solution.

All the following work is undertaken in the ViewController project:

1) New class: JdbcDCErrorHandlerImpl.java
package view;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

import java.sql.SQLException;

import oracle.adf.model.binding.DCBindingContainer;
import oracle.adf.model.binding.DCErrorHandlerImpl;

import oracle.jbo.DMLException;
import oracle.jbo.common.JBOClass;

/*
* Example sourced from Steve Muench example #129
*/
public class JdbcDCErrorHandlerImpl extends DCErrorHandlerImpl {
public JdbcDCErrorHandlerImpl() {
super(true);
}

public JdbcDCErrorHandlerImpl(boolean b) {
super(b);
}

private static final int INVALID_USERNAME_PASSWORD_ORACLE_ERROR = 1017;
private static final int ACCOUNT_LOCKED_ORACLE_ERROR = 28000;
private static final int NO_SUITABLE_DRIVER = 0;
private static final int NETWORK_CONNECTION_ERROR = 17002;
private static final int NETWORK_ADAPTOR_ERROR = 20;

public static boolean isFailedDBConnectErrorCode(SQLException s) {
int errorCode = s.getErrorCode();
return (errorCode == INVALID_USERNAME_PASSWORD_ORACLE_ERROR || errorCode == ACCOUNT_LOCKED_ORACLE_ERROR ||
errorCode == NO_SUITABLE_DRIVER || errorCode == NETWORK_CONNECTION_ERROR ||
errorCode == NETWORK_ADAPTOR_ERROR);
}

@Override
public void reportException(DCBindingContainer formBnd, Exception e) {
super.reportException(formBnd, e);
if (e instanceof DMLException) {
Object[] details = ((DMLException)e).getDetails();
if (details != null && details.length > 0) {
if (details[0] instanceof SQLException) {
SQLException s = (SQLException)details[0];
int errorCode = s.getErrorCode();
if (isFailedDBConnectErrorCode(s)) {
markResponseCompleteIfUsingJSF();
throw (DMLException)e;
}
}
}
}
}

/*
* If we are running in a Faces environment, invoke the FacesContext.responseComplete() method after
* the session invalidate. We use Java reflection so that our code can still work in a Non-Faces environment, too.
*/
private void markResponseCompleteIfUsingJSF() {
try {
Class c = JBOClass.forName("javax.faces.context.FacesContext");
Method m = c.getMethod("getCurrentInstance", null);
Object obj = m.invoke(null, null);
if (obj != null) {
m = c.getMethod("responseComplete", null);
m.invoke(obj, null);
}
} catch (InvocationTargetException ex) {
throw new RuntimeException(ex);
} catch (IllegalAccessException ex) {
throw new RuntimeException(ex);
} catch (NoSuchMethodException ex) {
throw new RuntimeException(ex);
} catch (ClassNotFoundException ex) {
// Ignore, we're not running in a faces context.
}
}
}
2) New class: JdbcPagePhaseListener.java
package view;

import java.util.ArrayList;
import java.util.List;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;

import oracle.adf.controller.v2.lifecycle.Lifecycle;
import oracle.adf.controller.v2.lifecycle.PagePhaseEvent;
import oracle.adf.controller.v2.lifecycle.PagePhaseListener;
import oracle.adf.model.bc4j.DCJboDataControl;
import oracle.adf.model.binding.DCBindingContainer;
import oracle.adf.model.binding.DCDataControl;
import oracle.adf.model.binding.DCExecutableBinding;
import oracle.adf.model.binding.DCIteratorBinding;

import oracle.binding.DataControl;

import oracle.jbo.uicli.binding.JUControlBinding;
import oracle.jbo.uicli.binding.JUCtrlActionBinding;

/*
* Example sourced from Steve Muench example #129
*/
public class JdbcPagePhaseListener implements PagePhaseListener {
public JdbcPagePhaseListener() {
}

public void afterPhase(PagePhaseEvent event) {
}

private HttpSession getHttpSession(PagePhaseEvent event) {
return ((HttpServletRequest)event.getLifecycleContext().getEnvironment().getRequest()).getSession(true);
}

public void beforePhase(PagePhaseEvent event) {
if (event.getPhaseId() == Lifecycle.PREPARE_MODEL_ID) {
DCBindingContainer bc = (DCBindingContainer)event.getLifecycleContext().getBindingContainer();
// Force the Data Control to be referenced before the prepareModel phase to cause the possible JDBC connection
// failure to be signalled now instead of during the page rendering.
List dcList = getADFBCDataControlsList(bc);
}
}

/*
* Return a list of ADFBC data controls used by this page. See how this is used in the beforePhase method above.
*/
private List getADFBCDataControlsList(DCBindingContainer bc) {
List dcList = null;
// if bc == null, means non data bound page, as such no data controls to exercise
if (bc != null) {
dcList = new ArrayList();
List ctrlBindings = (List)bc.getControlBindings();
if (ctrlBindings != null) {
for (JUControlBinding ctrlBinding : ctrlBindings) {
DCIteratorBinding iter = ctrlBinding.getIteratorBinding();
DCDataControl dc = null;
if (iter != null) {
dc = iter.getDataControl();
} else if (ctrlBinding instanceof JUCtrlActionBinding) {
dc = ((JUCtrlActionBinding)ctrlBinding).getDataControl();
}
if (dc != null && dc instanceof DCJboDataControl && !dcList.contains(dc)) {
DCJboDataControl bcdc = (DCJboDataControl)dc;
dcList.add(bcdc);
}
}
}
List exeBindings = (List)bc.getIterBindingList();
if (exeBindings != null) {
for (DCExecutableBinding exeBinding : exeBindings) {
DataControl dc = null;
if (exeBinding instanceof DCIteratorBinding) {
dc = ((DCIteratorBinding)exeBinding).getDataControl();
}
if (dc != null && dc instanceof DCJboDataControl && !dcList.contains(dc)) {
DCJboDataControl bcdc = (DCJboDataControl)dc;
dcList.add(bcdc);
}
}
}
}
return dcList;
}
}
3) New file: ViewController/adfmsrc/META-INF/adf-settings.xml
 



JdbcPagePhaseListener
view.JdbcPagePhaseListener



4) Modify the DataBinding.cpx file ErrorHandlerClass property
             SeparateXMLFiles="false" Package="view" ClientType="Generic" ErrorHandlerClass="view.JdbcDCErrorHandlerImpl">
5) Add an error-page entry in the web.xml

oracle.jbo.DMLException
/ServiceUnavailable.html
6) Add the corresponding html page from the last entry, displaying whatever friendly error message you want to show.