Tuesday 30 November 2010

Using ojdeploy and Ant for creating ADF library JARs

Projects and the Application itself in Oracle's JDeveloper 11g are capable of generating different deployment files at design time, including WAR files, EAR files, JAR files and other standard Java EE archive types. In addition JDev can generate a special JAR type specific to ADF development known as an ADF library. An ADF library differs from standard JARs in that the ADF library includes additional metadata files and constructs required for ADF application development.

To create an ADF library JAR you first need to setup an associated ADF library JAR via the project properties. This is well document in the Fusion Guide.

From here there are essentially 2 ways to actually generate the ADF library JAR. The first is within the IDE via the project right-click deploy options, and applicable to programmers developing in their JDev IDE.

The other option particularly important to build environments is the ojdeploy utility. This tool can be called via the command line or an Ant script, and is necessary for generating ADF library JAR files with the required metadata files and other ADF constructs.

In specifically considering the ant script option for ojdeploy, JDev will create a basic Ant script for building your applications via the New Gallery -> Ant -> Buildfile from Project option. In the dialog that displays selecting the "Include Packaging Tasks (uses ojdeploy)" checkbox ensures the generated Ant script includes an ojdeploy target:

The resulting Ant file will look something like this:
<?xml version="1.0" encoding="UTF-8" ?>
<project name="ViewController" default="all" basedir=".">
<property file="build.properties"/>
<path>....snipped....</path>
<target name="init">
<tstamp/>
<mkdir dir="${output.dir}"/>
</target>
<target name="all" description="Build the project" depends="deploy,compile,copy"/>
<target name="clean" description="Clean the project">
<delete includeemptydirs="true" quiet="true">
<fileset dir="${output.dir}" includes="**/*"/>
</delete>
</target>
<target name="deploy" description="Deploy JDeveloper profiles" depends="init,compile">
<taskdef name="ojdeploy" classname="oracle.jdeveloper.deploy.ant.OJDeployAntTask" uri="oraclelib:OJDeployAntTask"
classpath="${oracle.jdeveloper.ant.library}"/>
<ora:ojdeploy xmlns:ora="oraclelib:OJDeployAntTask" executable="${oracle.jdeveloper.ojdeploy.path}"
ora:buildscript="${oracle.jdeveloper.deploy.dir}/ojdeploy-build.xml"
ora:statuslog="${oracle.jdeveloper.deploy.dir}/ojdeploy-statuslog.xml">
<ora:deploy>
<ora:parameter name="workspace" value="${oracle.jdeveloper.workspace.path}"/>
<ora:parameter name="project" value="${oracle.jdeveloper.project.name}"/>
<ora:parameter name="profile" value="${oracle.jdeveloper.deploy.profile.name}"/>
<ora:parameter name="nocompile" value="true"/>
<ora:parameter name="outputfile" value="${oracle.jdeveloper.deploy.outputfile}"/>
</ora:deploy>
</ora:ojdeploy>
</target>
<target name="compile" description="Compile Java source files" depends="init">
<javac destdir="${output.dir}" classpathref="classpath" debug="${javac.debug}" nowarn="${javac.nowarn}"
deprecation="${javac.deprecation}" encoding="UTF-8" source="1.6" target="1.6">
<src path="src"/>
</javac>
</target>
<target name="copy" description="Copy files to output directory" depends="init">
<patternset id="copy.patterns">
<include name="**/*.gif"/>
<include name="**/*.jpg"/>
<include name="**/*.jpeg"/>
<include name="**/*.png"/>
<include name="**/*.properties"/>
<include name="**/*.xml"/>
<include name="**/*.ejx"/>
<include name="**/*.xcfg"/>
<include name="**/*.cpx"/>
<include name="**/*.dcx"/>
<include name="**/*.sva"/>
<include name="**/*.wsdl"/>
<include name="**/*.ini"/>
<include name="**/*.tld"/>
<include name="**/*.tag"/>
<include name="**/*.xlf"/>
<include name="**/*.xsl"/>
<include name="**/*.xsd"/>
</patternset>
<copy todir="${output.dir}">
<fileset dir="src">
<patternset refid="copy.patterns"/>
</fileset>
</copy>
</target>
</project>
In particular notice the Ant targets: init, clean, deploy, compile and copy. Note within the deploy target a call to the ojdeploy utility.

Now you'd think with these 5 targets generated by JDeveloper that they are all co-related and have dependencies. You can even see the deploy-ojdeploy target has dependencies on the init and compile targets. Unfortunately this is misleading.

The ojdeploy utility written by Oracle is in fact capable of doing all the other targets' tasks. The utility takes care of creating/initializing the destination directories, it cleans and compiles the specified application or project, and it takes care of copying all class files. In fact if you leave the other targets in the Ant script this can interfere with the operation of the ojdeploy utility and you should in fact remove them. Instead you should have something like this:

<project name="ViewController" default="all" basedir=".">
<property file="build.properties"/>
<path>....snipped....
<target name="deploy" description="Deploy JDeveloper profiles" depends="init,compile">
<taskdef name="ojdeploy" classname="oracle.jdeveloper.deploy.ant.OJDeployAntTask" uri="oraclelib:OJDeployAntTask"
classpath="${oracle.jdeveloper.ant.library}"/>
<ora:ojdeploy xmlns:ora="oraclelib:OJDeployAntTask" executable="${oracle.jdeveloper.ojdeploy.path}"
ora:buildscript="${oracle.jdeveloper.deploy.dir}/ojdeploy-build.xml"
ora:statuslog="${oracle.jdeveloper.deploy.dir}/ojdeploy-statuslog.xml">
<ora:deploy>
<ora:parameter name="workspace" value="${oracle.jdeveloper.workspace.path}"/>
<ora:parameter name="project" value="${oracle.jdeveloper.project.name}"/>
<ora:parameter name="profile" value="${oracle.jdeveloper.deploy.profile.name}"/>
<ora:parameter name="outputfile" value="${oracle.jdeveloper.deploy.outputfile}"/>
</ora:deploy>
</ora:ojdeploy>
</target>
</project>
If you've looked very carefully, you might have noticed I removed the nocompile option for ojdeploy. Bizarrely this boolean option takes three forms: true, false, and, the one you need to use to ensure the correct ADF constructs (such as task-flow-registry.xml) are added to the end ADF library JAR, is remove the nocompile option completely.

(This is bug 9000629 – closed by Support, not considered a bug, but confusing and not intuitive by design)

On the Windows platform you need to be careful that any paths you include in the ojdeploy target within the Ant script (either directly or indirectly via a properties file) exactly match the case of the Windows file system, otherwise similar issues can occur.

(Another bug 10028816 – confirmed bug – fixed 11.1.1.4.0 – patch available for 11.1.1.2.0)

Finally there's also a bug in the ojdeploy workspace option that it can't support the Ant script ${user.dir} property. This isn't applicable to the example above but I thought worth documenting.

(Bug 10028879 – confirmed bug – fixed 11.1.1.4.0 – patch available for 11.1.1.2.0)

Entirely separately to generating ADF libraries, if you wish to use the ojdeploy utility to create an EAR via the workspace, you do this by dropping the project option leaving the workspace, profile and outputfile options. If you do this under JDev 11.1.1.2.0 specifically you'll see the error message "Missing <workspace>, <project> or <profile> parameter in <deploy> element", caused by a bug in the ojdeploy utility, of which there is a patch available.

(Bug 9135159 – confirmed bug – fixed 11.1.1.3.0 – patch available for 11.1.1.2.0)

Addendum

All the above documented via JDev 11.1.1.2.0.

Wednesday 24 November 2010

JDeveloper REST Web Services presentation available

For those who attended the AUSOUG'10 Perth conference, my presentation JDeveloper 11g's REST web services is now available for download.

Thanks to all the members who voted for this session, I was well chuffed when it received 2nd best presentation at the conference.

Tuesday 16 November 2010

Hudson Job SVN "Malformed URL" error

A blog post documenting something we configured incorrectly using Hudson, maybe useful to others.

Via Hudson we build a Java EE application using Ant. The associated Hudson job first checks out the code from SVN. During the SVN check out phase of the job we'd see the following reported in the job console output:

Started by user cmuir
Checking out a fresh workspace because there's no workspace at C:\java\hudson\Hudson1372\build
Checking out https://acme.com/svn/VisualSVNRepo/ADFTaskFlows/REA/trunk
A Build
A Build\Build.jpr
A Build\rea.build.properties
A Build\rea.hudson.properties
A Build\build.xml
A Model
... snip ...
A ViewController\src\acme\rea\taskflow\view\beans\pageFlow
A ViewController\src\acme\rea\taChecking out
ERROR: Failed to check out
org.tmatesoft.svn.core.SVNException: svn: Malformed URL ''
at org.tmatesoft.svn.core.internal.wc.SVNErrorManager.error(SVNErrorManager.java:64)
at org.tmatesoft.svn.core.internal.wc.SVNErrorManager.error(SVNErrorManager.java:51)
at org.tmatesoft.svn.core.SVNURL.(SVNURL.java:221)
at org.tmatesoft.svn.core.SVNURL.parseURIEncoded(SVNURL.java:125)
at hudson.scm.SubversionSCM$ModuleLocation.getSVNURL(SubversionSCM.java:2116)
at hudson.scm.SubversionSCM$CheckOutTask.invoke(SubversionSCM.java:742)
at hudson.scm.SubversionSCM$CheckOutTask.invoke(SubversionSCM.java:660)
at hudson.FilePath.act(FilePath.java:753)
at hudson.FilePath.act(FilePath.java:735)
at hudson.scm.SubversionSCM.checkout(SubversionSCM.java:653)
at hudson.scm.SubversionSCM.checkout(SubversionSCM.java:601)
at hudson.model.AbstractProject.checkout(AbstractProject.java:1046)
at hudson.model.AbstractBuild$AbstractRunner.checkout(AbstractBuild.java:479)
at hudson.model.AbstractBuild$AbstractRunner.run(AbstractBuild.java:411)
at hudson.model.Run.run(Run.java:1248)
at hudson.model.FreeStyleBuild.run(FreeStyleBuild.java:46)
at hudson.model.ResourceController.execute(ResourceController.java:88)
at hudson.model.Executor.run(Executor.java:129)
skflow\view\beans\pageFlow\pageFlow
A ViewController\src\acme\rea\taskflow\view\beans\pageFlow\AuditEvent.java
... snip ...
At revision 3642
Sending e-mails to: chris.muir@acme.com
Finished: FAILURE
In particular note the error:

ERROR: Failed to check out
org.tmatesoft.svn.core.SVNException: svn: Malformed URL ''

From a bit of research we discovered the problem in our specific case was an error in the Hudson job configuration (Hudson console -> Login -> (select specific job) -> Configure). Under the Source Code Management section we'd accidentally created two SVN locations, the 2nd one being blank:


Essentially this second blank entry was causing the error above, where the Malformed "empty" URL '' is the empty SVN location. Simply removing this location from the job configuration solves the issue.

Addendum

Hudson 1.372

Tuesday 9 November 2010

JDev: Programmatically capturing task flow parameters

We recently had the requirement to log all incoming and outgoing parameters from Bounded Task Flows (BTF) for JDeveloper 11g. Via the kind assistance of Simon Lessard and other OTN Forum helpers (of whom I'm very grateful) we were able to come up with the following solution. I share it here for others to benefit from Simon's advice.

Warning

As per the OTN thread the following solution makes use of "internal" ADF libraries which Oracle gives no guarantee will not change in the future. The following code was tested under 11.1.1.2.0 and is assumed to also work in 11.1.1.3.0, yet you should check carefully that this code works in future versions, as we said, there's no guarantees it will performed as required. Also note Frank Nimphius has raised ER 10198616 to request a public API for the internal ADF libraries used in this solution.

Solution

Developers who are familiar with BTFs in JDev will know that they have an initializer and finalizer property which via EL can call which ever bean code we desire. The following code solution is simply the EL method end points which are called.

I won't bother to explain the code solution, it should be fairly self explanatory:

public class SomeClass {

public void initializer() {
Map taskFlowInputParameters = TaskFlowUtils.getCurrentTaskFlowInputParameters();
logBtfParameters(taskFlowInputParameters);
}

public void finalizer() {
Map taskFlowReturnParameters = TaskFlowUtils.getCurrentTaskFlowReturnParameters();
logBtfParameters(taskFlowReturnParameters);
}

public void logBtfParameters(Map btfParameters) {
HashMap taskFlowParameterValues = new HashMap();

FacesContext facesContext = FacesContext.getCurrentInstance();
Application application = facesContext.getApplication();
AdfFacesContext adfFacesContext = AdfFacesContext.getCurrentInstance();
Map pageFlowScope = adfFacesContext.getPageFlowScope();

for (Object parameter : btfParameters.values()) {
NamedParameter namedParameter = (NamedParameter)parameter;
String parameterName = namedParameter.getName();
String parameterExpression = namedParameter.getValueExpression();
Object parameterValue;
String stringValue;

if (parameterExpression == null) {
parameterValue = pageFlowScope.get(parameterName);
} else {
parameterValue = application.evaluateExpressionGet(facesContext, parameterExpression, Object.class);
}

if (parameterValue != null) {
try {
stringValue = parameterValue.toString();
} catch (Exception e) {
stringValue = "";
}
} else {
stringValue = "";
}

taskFlowParameterValues.put(parameterName, stringValue);
}
// log the taskFlowParameterValues parameters somewhere
}

}
The code above makes use of the following custom task flow utility class:

import java.util.Map;

import oracle.adf.controller.ControllerContext;
import oracle.adf.controller.TaskFlowContext;
import oracle.adf.controller.TaskFlowId;
import oracle.adf.controller.ViewPortContext;
import oracle.adf.controller.internal.metadata.MetadataService;
import oracle.adf.controller.internal.metadata.NamedParameter;
import oracle.adf.controller.internal.metadata.TaskFlowDefinition;
import oracle.adf.controller.internal.metadata.TaskFlowInputParameter;


/*
* Note this class makes of "internal" classes that Oracle preferred we didn't use (as there's no
* guarantee they wont change. However as of JDev 11.1.1.2.0 there is no other solution for
* retrieving task flow parameter names.
*
* See: http://forums.oracle.com/forums/thread.jspa?threadID=1556568&start=0&tstart=0
*
* Oracle has raised ER 10198616 to create a public API for the internal classes in this case.
*/
public class TaskFlowUtils {

public static TaskFlowId getTaskFlowId() {
ControllerContext controllerContext = ControllerContext.getInstance();
ViewPortContext currentViewPort = controllerContext.getCurrentViewPort();
TaskFlowContext taskFlowContext = currentViewPort.getTaskFlowContext();
TaskFlowId taskFlowId = taskFlowContext.getTaskFlowId();

return taskFlowId;
}

public static TaskFlowDefinition getTaskFlowDefinition(TaskFlowId taskFlowId) {
assert taskFlowId != null;

MetadataService metadataService = MetadataService.getInstance();
TaskFlowDefinition taskFlowDefinition = metadataService.getTaskFlowDefinition(taskFlowId);

return taskFlowDefinition;
}

public static Map getCurrentTaskFlowInputParameters() {
return getInputParameters(getTaskFlowId());
}

public static Map getInputParameters(TaskFlowId taskFlowId) {
assert taskFlowId != null;

TaskFlowDefinition taskFlowDefinition = getTaskFlowDefinition(taskFlowId);
Map taskFlowInputParameters = taskFlowDefinition.getInputParameters();

return taskFlowInputParameters;
}

public static Map getCurrentTaskFlowReturnParameters() {
return getReturnParameters(getTaskFlowId());
}

public static Map getReturnParameters(TaskFlowId taskFlowId) {
assert taskFlowId != null;

TaskFlowDefinition taskFlowDefinition = getTaskFlowDefinition(taskFlowId);
Map namedParameters = taskFlowDefinition.getReturnValues();

return namedParameters;
}
}
Caveat

This code hasn't yet been extensively tested, and in general just shows the programmatic technique for others to understand. Internally we've already generalized this code further to suit our own use case. If you find any bugs or obvious errors with the technique, leave a comment describing the problem you found and any solutions please.

Wednesday 3 November 2010

ADF UI Shell: Supporting global hotkeys for the activity tabs

We recently had the requirement to provide our users global hotkeys to switch between tabs within the ADF UI Shell (a.k.a. Dynamic Tab Shell) in JDev 11g. Luckily I caught a presentation at Open World this year where Frank Nimphius showed off support for creating global hotkeys in ADF applications. Frank was kind enough to give me his source code allowing me to modify it to suit the ADF UI Shell specifically. The following code shows the general technique for getting global hotkeys workings, as well as specific code to suit the ADF UI Shell implementation. The following code was built and tested under JDev 11.1.1.2.0.

ADF UI Shell Extension – global hotkeys

Readers familiar with the current incarnation of the ADF UI Shell will know it has the ability to spawn 1 to 15 "activity" tabs displaying whatever the programmer chooses. This provides an ideal framework for users to spawn multiple bounded task flows (BTFs) and work on several data sets all within the one browser window.

Our users have taken to the UI Shell and have been asking for extensions ever since. Of particular note they wanted the ability to open a set of activity tabs, and then flip between them using keyboard shortcuts rather than mouse clicks.

Frank's solution to the rescue.

Solution

The solution requires 3 working parts:

a) JavaScript
b) ViewScoped Managed Bean
c) Changes to the ADF UI Shell derived page to support the hotkeys to call "a" and "b"

The following code shows the general technique.

JavaScript
var keyRegistry = new Array();

keyRegistry[0] = "alt 1";
keyRegistry[1] = "alt 2";
keyRegistry[2] = "alt 3";
keyRegistry[3] = "alt 4";
keyRegistry[4] = "alt 5";
keyRegistry[5] = "alt 6";
keyRegistry[6] = "alt 7";
keyRegistry[7] = "alt 8";
keyRegistry[8] = "alt 9";
keyRegistry[9] = "alt 0";

function registerKeyBoardHandler(serverListener, afdocument) {

_serverListener = serverListener;
var document = AdfPage.PAGE.findComponentByAbsoluteId(afdocument);
_document = document;

for (var i = keyRegistry.length - 1; i >= 0; i--) {
var keyStroke = AdfKeyStroke.getKeyStrokeFromMarshalledString(keyRegistry[i]);
AdfRichUIPeer.registerKeyStroke(document, keyStroke, callBack);
}
}

function callBack(keyCode) {
var activeComponentClientId = AdfPage.PAGE.getActiveComponentId();

// Send the marshalled key code to the server listener for the developer
// To handle the function key in a managed bean method
var marshalledKeyCode = keyCode.toMarshalledString();

// {AdfUIComponent} component Component to queue the custom event on
// {String} name of serverListener
// {Object} params a set of parameters to include on the event.
// {Boolean} immediate whether the custom event is "immediate" -
// which will cause it to be delivered during Apply Request
// Values on the server, or not immediate, in which
// case it will be delivered during Invoke Application.

// Note that if one of the keyboard functions is to create ADF
// bound rows, immediate must be set to false. There is no
// option yet for the ClientEvent to be queued for later -
// InvokeApplication - on the server.
AdfCustomEvent.queue(_document,
_serverListener, {keycode:marshalledKeyCode,
activeComponentClientId:activeComponentClientId}, false);

// indicate to the client that the key was handled and that there
// is no need to pass the event to the browser to handle it
return true;
}
You'll note the hotkey mapping only goes upto 10 (1 to 9 plus zero). The UI Shell does support 15 tabs, but I couldn't think of a nice key combination beyond the 10th.... that can be left up to you.

ViewScoped Managed Bean
import java.util.List;

import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.faces.event.PhaseEvent;
import javax.faces.event.PhaseId;

import oracle.adf.view.rich.render.ClientEvent;

import org.apache.myfaces.trinidad.render.ExtendedRenderKitService;
import org.apache.myfaces.trinidad.util.Service;

import waosr.ui.pattern.dynamicShell.TabContext;


public class KeyboardHandler {

public void registerKeyboardMapping(PhaseEvent phaseEvent) {
if (phaseEvent.getPhaseId() == PhaseId.RENDER_RESPONSE) {

FacesContext facesContext = FacesContext.getCurrentInstance();
ExtendedRenderKitService extRenderKitService =
Service.getRenderKitService(facesContext, ExtendedRenderKitService.class);
List<UIComponent> childComponents = facesContext.getViewRoot().getChildren();
//First child component in an ADF Faces page - and the only child - is af:document
//Thus no need to parse the child components and check for their component family
//type
String id = ((UIComponent)childComponents.get(0)).getClientId(facesContext);

StringBuffer script = new StringBuffer();
script.append("window.registerKeyBoardHandler('keyboardToServerNotify','" + id + "')");
extRenderKitService.addScript(facesContext, script.toString());
}
}

public void handleKeyboardEvent(ClientEvent clientEvent) {

String keyCode = (String)clientEvent.getParameters().get("keycode");

// The alt+<number> keyboard combination opens the relating ADF UI Shell tab if open
if (keyCode.equalsIgnoreCase("alt 1") || keyCode.equalsIgnoreCase("alt 2") || keyCode.equalsIgnoreCase("alt 3") ||
keyCode.equalsIgnoreCase("alt 4") || keyCode.equalsIgnoreCase("alt 5") || keyCode.equalsIgnoreCase("alt 6") ||
keyCode.equalsIgnoreCase("alt 7") || keyCode.equalsIgnoreCase("alt 8") || keyCode.equalsIgnoreCase("alt 9")) {

String keyIndexStr = keyCode.substring(keyCode.length() - 1, keyCode.length());
int keyIndex = Integer.parseInt(keyIndexStr);
TabContext.getCurrentInstance().setSelectedTabIndex(keyIndex - 1);
}
}
}
Of specific importance the handleKeyboardEvent() method makes use of the ADF UI Shell TabContext class to set the current activity-tab according to the hotkey pressed.

ADF UI Shell page

xmlns:h="http://java.sun.com/jsf/html" xmlns:af="http://xmlns.oracle.com/adf/faces/rich">







method="#{viewScope.keyboardHandler.handleKeyboardEvent}"/>
value="#{bindings.pageTemplateBinding}" id="pt1">






Note:

a) The beforePhase property in the view tag
b) The loading of the JavaScript via the resource tag
c) The serverlistener that calls the bean methods

How This Works

1) Before the page is rendered, JavaScript is added to call the registerKeyBoardHandler method in the attached JavaScript library when the page is rendered
2) The JavaScript method registers (but does not invoke) for the defined keys a server side event
3) On the page rendering, if the user clicks one of the keys, the interaction of the JavaScript server side event *and* the serverListener in the page calls the bean handleKeyboardEvent method
4) Finally the method calls the ADF UI Shell TabContext class to switch tabs based on the hotkey number

Caveat

Frank notes some minor cross browser compatibility issues and certain hotkey combinations not working. Rather than highlighting the specific problems in this release, as this solution is reliant on JavaScript the ever unreliable-pain-in-the-butt-that-it-is-across-different-browser-versions, readers are highly recommended to do their own cross-browser testing.