Working with Custom Resources
Writing
the implementing Java class
Creating
a Custom Resource in the Ebase Designer
Coding
script commands to access the resource.
Ebase
supplied Custom Resources
See also: How
Resources Work
If you
are considering writing a Custom Resource, you should consider using Javascript as an alternative. A Javascript
script can access Java classes directly and can in nearly all circumstances
achieve the same result as a Custom Resource with much less effort. Click here for details.
A
custom resource represents a connection to an external system which is
implemented by a customer written Java class. This gives the ability to connect
to any external system that has an API that can be addressed by Java. Once
created, the custom resource appears to the Ebase designer exactly the same as
any other resource i.e. it can be added to a form, its fields can be imported and
mapped, script statements can be written to interchange data between a form and
the external system. A custom resource can be thought of as a plug-in to add
integration to a specific external system.
The Java
class implementing a custom resource is instantiated the first time the
resource is invoked via a script statement and the same instance is used for
subsequent calls for the duration of the form. It is passed the invoking script
command and an interface allowing the class to access the resource fields - to
check characteristics and to get and set values.
A custom
resource can process both tabular and non-tabular data.
There are two steps to creating a custom resource - write the implementing Java class and then create the resource in the Ebase Designer.
The Java
class must implement interface com.ebasetech.ufs.mapping.CustomResourceInterface
which is included in the ufs.jar file. This
interface contains the following methods:
·
execute() is
the primary point of contact for Ebase with the resource and is called each
time a script command is issued against the resource. This method receives two
parameters: a ResourceRequestInterface object and a
String containing the issued script command. The ResourceRequestInterface
is a 'callback' interface and can be used to access
the resource fields to get and set values, check types and lengths etc, and
perform many other functions.
·
fetchTable() and updateTable()
are called when the fetchtable or updatetable FPL commands or API Table.fetchTable() or Table updateTable() methods are issued
against a table which is backed by the custom resource. These can be
implemented as empty methods if the custom resource does not provide support
for tables.
·
getParameterNames() is called by the Ebase Designer to find out
which parameters need to be supplied to the resource by the forms designer.
·
getCommandNames() is called by the Ebase Designer to find out
which script commands are supported by the resource. Any commands defined here
may be used in scripts against this resource. A list of
eligible commands are supplied as constants in ResourceRequestInterface
eg. COMMAND_FETCH, COMMAND_UPDATE etc. Customer
specific commands can also be used.
Please see
the following javadoc for details:
·
TableRow
ResourceRequestInterface also contains a number of static
constants for the possible script commands and field types.
Here is a simple
example of a custom resource Java class that supports the READ and WRITE script
commands in it's execute() method and does not support tabular data:
//
These imports are required
import com.ebasetech.ufs.mapping.CustomResourceInterface;
import com.ebasetech.ufs.mapping.ResourceRequestInterface;
import com.ebasetech.ufs.kernel.FormException;
import com.ebasetech.ufs.validation.CommandStatus;
public class ResourceTest implements CustomResourceInterface {
public String execute(ResourceRequestInterface
resourceInterface, String command) throws FormException {
// READ command
if ( command.equals(ResourceRequestInterface.COMMAND_READ) ) {
// code to set the resource fields from the external system goes here e.g.
resourceInterface.setFieldValue("FIELD1", "data for field 1");
resourceInterface.setFieldValue("FIELD2", "data for field 2");
}
// WRITE command
else if ( command.equals(ResourceRequestInterface.COMMAND_WRITE) ) {
// code to read the resource fields and write to the external system goes here
e.g.
Object a = resourceInterface.getFieldValue2("FIELD1");
Object b = resourceInterface.getFieldValue2("FIELD2");
}
return CommandStatus.STATUS_OK;
}
public void fetchTable( ResourceRequestInterface resourceRequest,
String fieldId ) throws FormException
{
}
public void updateTable( ResourceRequestInterface resourceRequest,
String fieldId ) throws FormException
{
}
public Collection getParameterNames()
{
ArrayList names = new ArrayList();
names.add( "PARM1" );
return names;
}
public Collection getCommandNames()
{
ArrayList names = new ArrayList();
names.add( ResourceRequestInterface.COMMAND_READ
);
names.add( ResourceRequestInterface.COMMAND_WRITE
);
return names;
}
}
The execute() method must return a string representing the
status of the script command. Ebase takes no action based on this status and
simply returns it to the script where it can be interrogated. The static
constants in class CommandStatus can be used as
status codes as shown in the example above, or
additional codes can be added as required.
e.g.
return "ERR1";
This can be
checked by a script:
FPL: |
API based language
(Javascript): |
read CUSTOM_RESOURCE1; if [ $COMMAND_STATUS = 'ERR1' ]
message 'Custom resource has returned error status ERR1'; endif |
var result =
resources.CUSTOM_RESOURCE1.execute(CustomResource.COMMAND_READ); if (result == "ERR1") { event.owner.addErrorMessage("Custom resource has
returned error status ERR1"); } |
If a condition occurs which should cause termination of the form, the method should throw a FormException. This will result in the exception's error message being written to the server log file, being displayed as an HTML page and written to the Ebase Designer execution log e.g.
throw new FormException("Custom
resource fatal error - cannot contact external system");
Resources
that support tables must also implement the fetchTable() and updateTable() methods. These are called
explicitly when the fetchtable
or updatetable
FPL script commands or API methods Table.fetchTable() or Table.updateTable() are issued against a table and the
table is backed by a custom resource. The name of the resource field which is
mapped to the table is passed as a parameter. (See Table
Concepts for more information)
The fetchTable()
method should do the following:
·
Get an empty TableData
object using resourceRequest.createNewTableData().
·
Populate this TableData
object with rows from the resource table. It can ask the resourceRequest
object which source fields are being used using resourceRequest.getFieldNames().
·
Set the newly populated TableData
back on the resourceRequest object using resourceRequest.setTableData().
The updateTable()
method should do the following:
·
Get the TableData from
the resourceRequest using resourceRequest.getTableData().
·
Interrogate the TableData
to find out which rows have been inserted/deleted/updated.
·
Apply these changes to the resource.
Here are
simple examples of implementation for both of these methods:
//
These imports are required in addition to those listed
above
import com.ebasetech.ufs.mapping.TableDataInterface;
import com.ebasetech.ufs.mapping.TableRow;
import com.ebasetech.ufs.mapping.TableCell;
import com.ebasetech.ufs.mapping.TableRowChangeStatus;
public void fetchTable( ResourceRequestInterface resourceRequest,
String fieldId ) throws FormException
{
Collection fieldNames
= resourceRequest.getFieldNames();
TableDataInterface tdi = resourceRequest.createNewTableData();
for each row in the table:
{
TableRow row = tdi.createNewRow();
for( Iterator iter
= fieldNames.iterator(); iter.hasNext();
)
{
String fieldName =
(String) iter.next();
row.addCell( fieldName,
"data for field" );
}
tdi.addRow( row );
}
resourceRequest.setTableData(fieldId, tdi );
}
public void updateTable( ResourceRequestInterface resourceRequest,
String fieldId ) throws FormException
{
TableDataInterface tdi = resourceRequest.getTableData( fieldId );
for( Iterator rowIter
= tdi.getChangedRows(); rowIter.hasNext();
)
{
TableRow row = (TableRow) rowIter.next();
int rowChangeStatus = row.getRowChangeStatus();
switch( rowChangeStatus )
{
case TableRowChangeStatus.DELETED:
// get the row key and delete it from this
resource
Object rowKey = row.getCell(
"key source field name" );
// use the row key to find the row in this
resource and delete it
break;
case TableRowChangeStatus.INSERTED:
// step through the row cells and collect
their values
for( Iterator cellIter = row.getTableCells(); cellIter.hasNext(); )
{
TableCell cell = (TableCell)
cellIter.next();
String fieldName = cell.getSourceFieldId();
Object cellValue = cell.getCurrentValue2();
// use this to build up the row to be added.
}
// add the row to this resource
break;
case TableRowChangeStatus.UPDATED:
// as above but use the values to update the
existing row in the resource.
break;
}
}
}
The getParameterNames() and getCommandNames() methods
should just return a simple collection of strings, for example:
private static final String RES_PARAM_PROTOCOL_TYPE = "ProtocolType";
private static final String RES_PARAM_PROTOCOL_STRING = "ProtcolString";
private static final String RES_PARAM_FILENAME_PREFIX = "FilenamePrefix";
private static final String COMMAND_SEND = "Send";
public Collection getParameterNames()
{
ArrayList names = new ArrayList();
names.add( RES_PARAM_PROTOCOL_TYPE );
names.add( RES_PARAM_PROTOCOL_STRING );
names.add( RES_PARAM_FILENAME_PREFIX );
return names;
}
public Collection getCommandNames()
{
ArrayList names = new ArrayList();
names.add( ResourceRequestInterface.COMMAND_READ
);
names.add( ResourceRequestInterface.COMMAND_WRITE
);
return names;
}
Then, for
example, whenever the resource implementation needs to fetch the value of one of
one of the declared parameters for a specific instance of that resource, it
should use:
resourceRequest.getParameterValue( RES_PARAM_FILENAME_PREFIX );
Create a custom resource by right clicking in the designer tree and selecting New > Custom Resource.
Enter the
fully qualified name of the java class implementing CustomResourceInterface.
This class must be on the classpath of the Ebase
Server web application (place jar file in ..../ufs/WEB-INF/lib
or class files in .../ufs/WEB-INF/classes). If
the class is found and implements CustomResourceInterface,
the panel below is displayed:
Resource
Description: provide a description of this custom resource.
The debug
checkbox can be checked by an implementing java class using method isDebug()
on ResourceRequestInterface.
Implementing
Java class: name of the implementing Java class
Resource
parameters: Parameters are defined by the CustomResourceInterface
implementation of the getParameterNames() method. This method should return a collection of the
parameter names to be populated for each instance of the custom resource. These
parameters can contain any information that is required to interface with the
external system. Typically, these fields provide information that uniquely
identifies this specific custom resource. Examples of this are file name and
type, transaction name, external system options etc.
(Note: for
backward compatibility, if the CustomResourceInterface.getParameterNames() method returns null, it will be assumed that this is an
old implementation and that the original 4 pre-defined parameters are still
being used. The getParameterN() methods will still operate as expected but it is recommended
that the implementation be migrated to explicitly declaring the parameters as
soon as possible.)
Implemented
script commands specifies a display only list of the script commands which are
implemented by the custom resource. If a script command is issued which is not
in this list, a runtime error will occur. Descriptions can be added here for
these commands.
The Resource
fields section allows you to specify the individual fields supported
by the resource. These can be added and deleted using the icons on the resource
fields toolbar or by right clicking on an existing
field.
The fields are
displayed as a tree that supports a hierarchical structure. This is primarily
to facilitate the support of XML document structures by a custom resource. Root
is always displayed as the first entry - this is a dummy field that does not
actually exist and is not saved in the repository database - it serves the role
of parent for all top level fields. A number of methods are supplied in ResourceRequestInterface to enable navigation through
the hierarchy, e.g.
getChildren()
getParent()
getChildCount()
hasChildren()
hasParent()
getTopLevelFieldNames()
If a
hierarchical structure is not required by a custom resource implementation, all
fields should be added as children of Root as shown in the example
below.
Field
Name supplies the name of the resource field and must be unique i.e. two
fields cannot have the same name. The field name can be changed by triple
clicking on the name.
External
Name can be used to represent the name of the field as it is known to the
external resource. External names do not need to be unique, however they should
be unique within any given parent otherwise it is not possible to locate a
given resource field using an external name. Two methods in ResourceRequestInterface provide support for external
name:
getFieldExternalName(String fieldName)
getFieldNameForExternalName( String externalName, String parentFieldName )
The length
attribute is used during import of a resource field into a form to set the
maximum allowable length for data entry. It is not used for any other purpose.
This can be checked by the custom resource using ResourceRequestInterface
method getFieldLength().
The decimal
digits attribute is used during import of a resource field into a form to
set the maximum allowable decimal digits for data entry. It is not used for any
other purpose. This can be checked by the custom resource using ResourceRequestInterface method getFieldDecimalDigits().
The Repeats
attribute indicates that the field and its children can occur more than once. Only
fields with this indicator set can be mapped to tables. This can be checked
by the custom resource using ResourceRequestInterface
method isFieldRepeatable().
The Key
attribute is not used by Ebase. It is intended to represent structural
information that can be used by a custom resource Java class, for example to
navigate through an XML document. This can be checked by the custom resource
using ResourceRequestInterface method isFieldKey().
Attribute
is only applicable for XML documents and indicates that the field is
represented by an attribute in the XML document. When unchecked, all fields are
represented by elements in the XML document. This can be checked by the custom
resource using ResourceRequestInterface method isFieldAttribute().
The field types
can be any of the Ebase supported field types (See Supported Field Types).
Save: saves the Custom Resource.
This icon is only shown for the Ebase provided
custom resources for XML and Web Services (now deprecated). It provides the
ability to automatically create a resource field hierarchy as specified in an
XML Schema (.xsd file) or WSDL (.wsdl
file). When clicked, a dialog is displayed allowing navigation to an xsd via either URL or local file browser. The resource
field attributes are set based on information in the schema. Where possible,
resource field names will be set the same as an imported element or attribute.
This is not possible when an element references a complex type, and in this
instance the field name will be prefixed with parent name. After the import,
the resource field names can be changed if required, by double clicking on the
name field. In all cases the external name will be set the same as the imported
element or attribute.
Maintain documentation for this resource.
Show information: dates for creation, last update and import of this Email Resource.
Shows this help page.
The field types supported by a custom resource and how these map to
Java objects is shown in the following table.
Resource field type |
Object returned from getFieldValue2() or TableCell.getCurrentValue2() |
Objects accepted for setFieldValue() or TableCell.setValue() |
CHAR |
String |
String, Integer, Long, Double, BigDecimal, Float, Date, Time, TimeStamp |
NUMBER |
BigDecimal |
String, Integer, Long, Double, BigDecimal, Float |
CURRENCY |
BigDecimal |
String, Integer, Long, Double, BigDecimal, Float |
INTEGER |
BigInteger |
String, Integer, Long, Double, BigDecimal, Float, BigInteger |
BOOLEAN |
Boolean |
Boolean, String |
DATE |
Long - suitable for constructing a java.util.Date (time element is set to midnight). |
Date, Time, TimeStamp, String |
TIME |
Long - suitable
for constructing a java.util.Date (date element set
to January 1, 1970) |
Date, Time, TimeStamp, String, Long, Number |
DATETIME |
Long - representing milliseconds since the epoch, January
1, 1970. Suitable for constructing a java.util.Date. |
Date, Time, TimeStamp, String, Long, Number |
If DATE
fields are set from a String, the String must be in the format specified by
parameter Ufs.dateFormat in file UFSSetup.properties.
If BOOLEAN
fields are set from a String, the String must contain either "Y" or
"N".
NOTE:
Fields that are used in tables will be returned in the
TableData in the form of the native Ebase data types.
They will NOT be converted back to the type native to the resource.
The custom
resource Java class is instantiated on the first call (i.e. as a result of the
first script command) from a form, and the same instance of the class is used
for all subsequent calls. Therefore, the class can maintain its own state, if
required. In addition:
· The custom
resource can get and set session context variables by obtaining the Http
session context with method getSessionContext() of ResourceRequestInterface.
· The custom
resource can share state variables with other forms by accessing the system
scratchpad area with methods addScratchPadObject(), getScratchPadObject(), removeScratchPadObject() of ResourceRequestInterface. The system scratchpad area is
stored in the web application context and is therefore available for the life
of the application server.
All custom
resource Java classes must be serializable. The state
is serialized and deserialized in two circumstances:
·
When the user clicks either the save or restore
buttons
·
By the application server when operating in a
clustered environment
Therefore,
all class level variables should also be serializable
(must implement java.io.Serializable). If this is not
possible for any reason, then any non-serializable
objects should be marked as transient and the class should be written so
that any such objects can be re-instantiated if necessary. If this condition is
not met, failures will occur in either of the two circumstances given above.
Ebase
maintains a transaction for each interaction with the end-user. Any
updates that are made by custom resources will be included within this
transactional context providing that the updates are made using the standard
facilities of the J2EE application server - in particular, that the resources
are accessed via the application server's JNDI naming context. The getInitialContext() method of ResourceRequestInterface
can be used to get the JNDI root context for addressing the application server.
All resources accessed in this way will be enlisted in to the transaction by
the J2EE application server.
In
addition, the existing Ebase transaction can be committed or rolled back using
methods commitTransaction() or rollbackTransaction() on ResourceRequestInterface. In both cases a new
transaction is started to contain any additional processing.
(See Transaction Support for more information)
Using FPL:
This is
exactly the same as for other external resources, e.g. databases. Any FPL script
command that is supported by the custom resource can be used.
e.g.
read RESOURCE1;
write RESOURCE2;
update RESOURCE3;
When issued, all such resource commands will result in a
call to the custom resource's execute()
method.
In addition, the fetchtable
and updatetable FPL commands can be used
for tables that are backed by a custom resource. These will result calls to the
custom resource's fetchtable() and updatetable()
methods.
All resource commands can specify a binding as an
optional parameter, e.g.
write BOOKING_XML systemx;
where systemx is a binding. This information is intended to provide the custom resource implementing class with additional information on the specific command being processed. Ebase takes no action based on the binding - it is simply passed through to the resource where it can be accessed by method getBinding() of ResourceRequestInterface.
Using API based language:
Use one of
the following two methods on CustomResource:
String execute(String
command);
String execute(String
command, String binding);
Where the second method allows
additional information to be passed into the resource as a binding. Both methods return a status String.
e.g.
var
status1 = resources.CUSTOM_RESOURCE1.execute(CustomResource.COMMAND_READ);
var
status2 = resources.CUSTOM_RESOURCE2.execute("Search", "Search
string");
The
following are all implementations of custom resource and have been created as
described above.
Represents a connection to an XML resource. The use of this class is now deprecated and has been replaced with XML resources.
Allows writing form data to a .csv file.
(See Working with CSV resources for more
information)
The use of this class is now deprecated and has been replaced with XML resources.
Java class XMLCustomResourceExample
is supplied as an example together with an example form. This class provides an
example of generic support for XML document structures. The supplied sample
form and XML document contain two repeating structures mapped to two tables. XPath is used to navigate within the XML document.
The example
consists of:
·
Form XML_EXAMPLE in project SAMPLES.
·
Custom Resource XML_EXAMPLE
·
Java class XMLCustomResourceExample.
·
XML file flights.xml
as shown below.
The Java class source and XML
document can be found in ufs\samples.
<Schedule>
<TDate>10 Nov 2004</TDate>
<Location>Cambridge</Location>
<Flights>
<Flight>
<FlightNo>XA123</FlightNo>
<Departure>08:00</Departure>
<Passengers>
<Passenger>
<Name>Fred Bloggs</Name>
<SpecialNeeds>Veggie</SpecialNeeds>
</Passenger>
<Passenger>
<Name>Anton Bloggs</Name>
<SpecialNeeds></SpecialNeeds>
</Passenger>
</Passengers>
</Flight>
<Flight>
<FlightNo>XA127</FlightNo>
<Departure>10:30</Departure>
<Passengers>
<Passenger>
<Name>Jo Cole</Name>
<SpecialNeeds></SpecialNeeds>
</Passenger>
<Passenger>
<Name>Michael Howard</Name>
<SpecialNeeds>Wheelchair</SpecialNeeds>
</Passenger>
</Passengers>
</Flight>
</Flights>
</Schedule>