Short summary over the steps described below:
Connector Senders are implemented as Java classes that dictate how the messages routed through the IFS Connect should be sent to a destination address, based on the native protocol (e.g. File I/O, SMTP, etc.) used for the communication. IFS Applications comes with a number of Connector Senders out of the box.
In this document we are going to show you how to develop your own custom Connector Sender. With that said, we recommend you to write a new connector sender only if none of the existing senders serves your requirement.
Before you can start with development of you custom Connect Sender you have to prepare a proper ANT project structure.
A complete example project can be found
here (the example is located in
workspace/fndint/source/fndint/connectors/my_connect_sender),
so preferably just copy the example to your component structure, adapt to your
needs and then open in NetBeans.
Connect Sender configuration is split into two parts: base configuration accessed through the Setup IFS Connect feature in Solution Manager and more specific one defined on particular Routing Address. To be possible to create a node in Setup IFS Connect corresponding to your sender you have to create Meta definitions as described in Connect Reader and Sender Configuration.
You have to create at least four Java classes based on abstract classes and
interfaces located in the Connect Framework JAR file, ifs-fnd-connect.jar
. Two of those classes will be responsible for the two parts of configuration
mentioned above. The two other are the actual sender implementation and a
factory class.
Start with implementing a class that extends
ifs.fnd.connect.config.ConnectorSendersConfig
(that in turn extends
ifs.fnd.connect.config.Config
). This class corresponds
to sender configuration in Setup IFS Connect. The class exposes all
necessary parameters as public final
variables. For that it has to
have a static inner class Builder
extending
ConnectorSendersConfig.Builder
, which is responsible for reading of
parameters, and a private constructor taking instance of this Builder
class as argument. First in the constructor call super(builder)
.
Next copy all the variables you want to have access to from your Sender
code from the Builder
instance to public final
variables in your Config class:
package ifs.fndint.connectreader; import ifs.fnd.base.IfsException; import ifs.application.fndconnectframework.ConfigParameter; import ifs.fnd.connect.config.Config; import
ifs.fnd.connect.config.ConnectorSendersConfig; public class MyConnectSenderConfig extends ConnectorSendersConfig { // inner Builder class static class Builder extends ConnectorSendersConfig.Builder { private String myVariable; // other variable declarations here ... // method implementations ... } // Config instance public final String myVariable; // other public final variables here ... private MyConnectSenderConfig(Builder builder) { super(builder); myVariable = builder.myVariable; // copy other variables here ... } }
Abstract methods that need to be overridden in the inner Builder
class:
protected void init(String name, ConfigParameter param) throws
IfsException
Implement a
switch
statement reading all your configuration
parameters (with exception of FACTORY_CLASS
, DEFAULT_INSTANCE
,
MAX_RETRIES
, RETRY_INTERVAL
and WORK_TIMEOUT
that are handled by the framework).
You can use methods
like replacePlaceholders()
and get*Value()
implemented
in the super-classes. Store the read values in private member variables:
@Override protected void init(String name, ConfigParameter param) throws IfsException { switch (name) { // read all sender specific parameters; common parameters: FACTORY_CLASS, // DEFAULT_INSTANCE, MAX_RETRIES, RETRY_INTERVAL and WORK_TIMEOUT are already // handled by the framework and don't need to be read here case "MY_FIRST_PARAMETER": myVariable = replacePlaceholders(getTextValue(name,param)); break; case "MY_SECOND_PARAMETER": myLongVariable = getNumberValue(name,param); break; // read other parameters here ... } }
protected void postInit() throws IfsException
If necessary, you can validate consistence of you parameters here. You can also
create new variables based on combinations of the read ones.
Note that an exception thrown from this method will prevent the server from
start, so if the user can enter an invalid combination of parameters it is
better to correct the values to a valid combination and write a warning to the
log file rather then throwing an exception:
@Override protected void postInit() throws IfsException { // any code that has to be executed after all parameters have been read, // e.g. parameter validation, consistency, variables that depend on several parameters, etc... }
protected Config newConfig()
This method is supposed to return a new instance of your Config (i.e.
ConnectorSendersConfig) class:
@Override protected Config newConfig() { return new MyConnectSenderConfig(this); }
Next you have to implement a parameterized class extending
ifs.fnd.connect.senders.addrcfg.AddressSenderConfig<C>
, where C
is the name of your first class, i.e. the one extending
ConnectorSendersConfig
.
This class is responsible for the part of configuration stored on Routing
Address. Necessary parameters are typically stored in attributes
addressData
, addressData2
and options
, but
there are more data you can extract from the context class. Note that the
options
attribute is parsed by the framework and you can obtain value of
a single option by calling the getOption()
method.
Even this class is constructed using an inner Builder
class
extending AddressSenderConfig.Builder<C>
where you read all the
necessary parameters.
Also here you have to implement a private
constructor taking the inner Builder
class as argument. Similarly
to the Config class above start with
calling super(builder)
, then copy all the variables you want to
have access to from your Sender code from the Builder
instance to public final
variables in your AddressSenderConfig
class.
package ifs.fndint.connectsender; import ifs.fnd.connect.senders.ConnectSender; import ifs.fnd.connect.senders.addrcfg.AddressSenderConfig; public class MyAddressSenderConfig extends AddressSenderConfig<MyConnectSenderConfig> { // inner Builder class public static class Builder extends AddressSenderConfig.Builder<MyConnectSenderConfig> { private transient String myVariable; // other variable declarations here ... // method implementations here ... } // AddressSenderConfig instance // address specific config parameters public final transient String myVariable; // other public final variables here ... private MyAddressSenderConfig(Builder builder) { super(builder); myVariable = builder.myVariable; // copy other variables here ... } }
Abstract methods that need to be overridden in the inner Builder
class:
protected void nativeInit() throws
ConnectSender.PermanentFailureException
Here you can read the necessary configuration data and store into private
variables. Note that you have access to many protected variables and methods
implemented by the super class. That way you can also access the first Config
class and also the Context class with additional methods that you can
call.
@Override protected void nativeInit() throws ConnectSender.PermanentFailureException { // initialize your member variables here // configuration data is typically stored in fields 'addressData' and 'addressData2' // that are available through the super class. Additional parameters can be stored in // options that can be obtained using getOption() from the super class. // you can also refer to variables initialized in your ConnectorSendersConfig // through 'config' field defined in the super class // and also use methods and fields, like replacePlaceholders(), from the context class // available through the 'ctx' field in the super class myVariable = ... }
public AddressSenderConfig newConfig()
This method is supposed to return a new instance of your AddressSenderConfig
class:
@Override public AddressSenderConfig newConfig() { return new MyAddressSenderConfig(this); }
Now its a time to implement the Sender itself. You will do it by
implementing a parameterized class extending
ifs.fnd.connect.senders.ConnectSender<C, A>
, where C
is
as above, the name of your class extending ConnectorSendersConfig
,
and A
the the name of your class extending
AddressSenderConfig
:
package ifs.fndint.connectsender; import ifs.fnd.connect.senders.ConnectSender; import javax.activation.DataSource; public class MyConnectSender extends ConnectSender<MyConnectSenderConfig, MyAddressSenderConfig> { // method implementations here ... }
Abstract methods that need to be overridden
protected byte[] nativeSend(List<DataSource> inputSources) throws
SenderFailureException
This is the only method you have to implement. This method does the work. Here
you have access to your both Config classes through protected variables
config
and addrCfg
. There are much more protected
variables and functions you can access from the code here.
The argument to the method is a list of data sources that are supposed to be
send by the sender. Typically there is only one source, but in some situations
it is possible to send several, for example to be send as attachments to a mail.
You can obtain main data source as a byte array by calling the
getMainInData()
method. As the second argument you specify the maximum
number of sources your Sender is supposed to handle, typically 1.
The method is supposed to return a response as an array of bytes. It can be data
received as a result of the sender action or just a simple receipt.
@Override protected byte[] nativeSend(List<DataSource> inputSources) throws SenderFailureException { // validate number of possible data sources and retrieve data to be sent byte[] inData = getMainInData(inputSources, 1); String responseMsg; try { // send the data here and prepare the response message responseMsg = ... } catch(AnyException e) {// error handling here; throw TemporaryFailureException or PermanentFailureException
} return Str.getUtf8Bytes(responseMsg); }
Other methods that may need to be overridden
There are some other methods already implemented in the super class ConnectSender that may need to be overridden in some situations. Those are:
public ConnectMessage.Type getResponseType()
The method implemented in the super class returns TEXT
. But if the
response from your Sender is in another format, you have to override this
method and return the proper type. If the type of the response your Sender
can return is not defined or your Sender can return responses in
different types, just return null
from the method, which will let
the framework to detect the proper response type.
public String getDefRespEncoding()
If the response from your Sender is any type of text (plain text or XML),
the default encoding will be UTF-8
. The framework will try to
decide encoding based on the
BOM bytes and
encoding specification in the XML header (if the response is an XML document),
but if not possible, the default encoding returned from this function will be
used or UTF-8
if not defined. The implementation from the super
class returns always UTF-8
, but you can override the method and
return something else, for example value of a corresponding configuration
parameter, if there is such in your Sender configuration.
public boolean compress()
The implementation in the super class returns value of the Compress
flag from the actual
Routing Address configuration. But some senders may not handle
compression or doing it in another way. Override this function and return
false
if you don't want the Batch Processor to zip the data before
sending it to your Sender.
Finally you have to implement a class implementing the interface
ifs.fnd.connect.senders.ConnectSenderFactory
. Fully qualified name of
this class you have to put as the default value of the
configuration parameter FACTORY_CLASS
in your
sender configuration. The class needs to implement
only three simple methods that create new instances of the classes you have
implemented above:
public ConnectorSendersConfig.Builder newConfigBuilder();
public AddressSenderConfig.Builder newAddressConfigBuilder();
public ConnectSender newConnectSender();
package ifs.fndint.connectsender; import ifs.fnd.connect.config.ConnectorSendersConfig; import ifs.fnd.connect.senders.ConnectSender; import ifs.fnd.connect.senders.ConnectSenderFactory; import ifs.fnd.connect.senders.addrcfg.AddressSenderConfig; /** * Custom Sender Factory class registered in the configuration. * The class is responsible for construction of both the sender class * itself and both config classes. */ public class MyConnectSenderFactory implements ConnectSenderFactory { @Override public ConnectorSendersConfig.Builder newConfigBuilder() { // constructs new instance of Config.Builder return new MyConnectSenderConfig.Builder(); } @Override public AddressSenderConfig.Builder<? extends ConnectorSendersConfig> newAddressConfigBuilder() { // constructs new instance of AddressSenderConfig.Builder return new MyAddressSenderConfig.Builder(); } @Override public ConnectSender<? extends ConnectorSendersConfig, ? extends AddressSenderConfig> newConnectSender() { // constructs new instance of Sender return new MyConnectSender(); } }
The ConnectSender
class defines two inner Exception
classes sharing a common super class SenderFailureException
. Those
are:
TemporaryFailureException
Throw this exception if the failure is of a temporary kind and there is a
chance the data could be successfully sent later on, on repeated execution – the
address line will be then marked as Retry and the application message
itself will get status Waiting and will be re-executed later on one or
more times depending on values of parameters MAX_RETRIES
and
RETRY_INTERVAL
you have on your sender
configuration.
PermanentFailureException
This exception is supposed to be thrown if it is not possible to recover from the error – the message will be then marked as Failed.