Skip to content

How to Develop a Transport Connector Sender

Quick Guide

Short summary over the steps described below:

  1. Prerequisites.
  2. Prepare project structure.
  3. Create class extending ConnectorSendersConfig
  4. Create class extending AddressSenderConfig
  5. Create class extending ConnectSender
  6. Create class implementing ConnectSenderFactory

Connector Senders

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. FTP, SMTP, etc.) used for the communication. IFS Cloud 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.

How to Develop a Connector Sender

Project Preparation

Before you can start with development of your 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.

Configuration

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 as described in Connect Reader and Sender Configuration.

Java Source Code

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.

  • Class extending ConnectorSendersConfig.
  • Class extending AddressSenderConfig.
  • Class extending ConnectSender.
  • Class implementing ConnectSenderFactory.

ConnectorSendersConfig

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.connectsender;  

import ifs.fnd.base.IfsException;  
import ifs.application.fndconnectframework.ConfigInstanceParam;  
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<br/> 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, ConfigInstanceParam 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":  
         mySecondVariable = replacePlaceholders(getTextValue(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);  
}

AddressSenderConfig

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 attribute CUSTOM_ADDRESS_DATA, but there are more data you can extract from the context class.
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);  
}

ConnectSender

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 ifs.fnd.util.IoUtil;  
import ifs.fnd.util.Str;  


import java.io.File;  
import java.io.IOException;  
import java.util.List;  
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.

ConnectSenderFactory

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();  
   }  
}

Error Handling

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.