Tuesday, November 1, 2011

Creating mock OFX server

Nowadays more and more financial establishments use modern Web technologies to provide user with better experience. One of such technologies is OFX.

OFX stands for Open Financial Exchange, it is the format, that is based on XML, for data exchange between financial institutions. It was released in 1997 and evolved from Microsoft's Open Financial Connectivity (OFC) and Intuit's Open Exchange file formats.

In Java there are not many OFX libraries, the best one is OFX4J. Using this library you can develop both client and server. It has classes for every possible OFX (XML) element. On my opinion, the biggest flaw of this library is the bad documentation. Unfortunately, there are also not many examples online on usage of OFX4J. So here is my example that may help someone.

Let's write the simple JAX-WS OFX server. As always at first we need to download the library or specify Maven dependency:
<dependency>
 <groupId>net.sf.ofx4j</groupId>
 <artifactId>ofx4j</artifactId>
 <version>1.5</version>
</dependency>

Now we need the web-service interface:
package com.test.ofx.server.service;

import javax.activation.DataHandler;
import javax.jws.WebMethod;
import javax.jws.WebService;

@WebService
public interface OFXServerService {    
    @WebMethod
    DataHandler getResponse(DataHandler request);    
}

As you can see there was nothing difficult. Now it's time for the implementation:

package com.test.ofx.server.service.impl;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.TreeSet;
import java.util.UUID;

import javax.activation.DataHandler;
import javax.jws.WebService;

import net.sf.ofx4j.domain.data.ApplicationSecurity;
import net.sf.ofx4j.domain.data.RequestEnvelope;
import net.sf.ofx4j.domain.data.RequestMessage;
import net.sf.ofx4j.domain.data.RequestMessageSet;
import net.sf.ofx4j.domain.data.ResponseEnvelope;
import net.sf.ofx4j.domain.data.ResponseMessageSet;
import net.sf.ofx4j.domain.data.banking.AccountType;
import net.sf.ofx4j.domain.data.banking.BankAccountDetails;
import net.sf.ofx4j.domain.data.banking.BankStatementRequestTransaction;
import net.sf.ofx4j.domain.data.banking.BankStatementResponse;
import net.sf.ofx4j.domain.data.banking.BankStatementResponseTransaction;
import net.sf.ofx4j.domain.data.banking.BankingResponseMessageSet;
import net.sf.ofx4j.domain.data.common.Status;
import net.sf.ofx4j.domain.data.common.Status.Severity;
import net.sf.ofx4j.io.AggregateMarshaller;
import net.sf.ofx4j.io.AggregateUnmarshaller;
import net.sf.ofx4j.io.OFXParseException;
import net.sf.ofx4j.io.OFXWriter;
import net.sf.ofx4j.io.v2.OFXV2Writer;

import com.sun.xml.ws.util.ByteArrayDataSource;
import com.test.ofx.server.service.OFXServerService;

@WebService(endpointInterface = "com.test.ofx.server.service.OFXServerService", name = "ofxWebService")
public class OFXServerServiceImpl implements OFXServerService {
    
    @Override
    public DataHandler getResponse(DataHandler request) {
        try {
            //parse request            
            AggregateUnmarshaller<requestenvelope> unmarshaller = new AggregateUnmarshaller<requestenvelope>(
                    RequestEnvelope.class);
            RequestEnvelope requestEnvelope = unmarshaller.unmarshal(request.getInputStream());

            ResponseEnvelope responseEnvelope = getResponse(requestEnvelope);

            //prepare response to send
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            OFXWriter writer = new OFXV2Writer(baos);
            AggregateMarshaller marshaller = new AggregateMarshaller();
            
            marshaller.marshal(responseEnvelope, writer);
            writer.close();
            DataHandler dataHandler = new DataHandler(new ByteArrayDataSource(baos.toByteArray(),
                    "application/octet-stream"));
            return dataHandler;
        } catch (IOException e) {
            throw new RuntimeException(e);
        } catch (OFXParseException e) {
            throw new RuntimeException(e);
        }
    }

    private ResponseEnvelope getResponse(RequestEnvelope requestEnvelope) {
        //declare variables to generate response
        ResponseEnvelope responseEnvelope = new ResponseEnvelope();            
        BankingResponseMessageSet responseMessageSet = new BankingResponseMessageSet();
        List<bankstatementresponsetransaction> transactions = new ArrayList<bankstatementresponsetransaction>();
        BankStatementResponse bankStatementResponse = new BankStatementResponse();
        BankAccountDetails bankAccountDetails;
        Random rand = new Random();

        //generate response transactions
        for (RequestMessageSet requestMessageSet : requestEnvelope.getMessageSets()) {
            for (RequestMessage requestMessage : requestMessageSet.getRequestMessages()) {
                //specify bank account details
                bankAccountDetails = new BankAccountDetails();
                bankAccountDetails
                        .setAccountNumber(((BankStatementRequestTransaction) requestMessage)
                                .getMessage().getAccount().getAccountNumber()
                                + "-" + rand.nextInt());
                bankAccountDetails.setBankId("Bank ID");
                bankAccountDetails.setAccountType(AccountType.CHECKING);
                
                //specify and add bank statment response transaction
                bankStatementResponse.setAccount(bankAccountDetails);
                BankStatementResponseTransaction bankStatementResponseTransaction = new BankStatementResponseTransaction();
                bankStatementResponseTransaction.setMessage(bankStatementResponse);
                Status status = new Status();
                status.setSeverity(Severity.INFO);        
                bankStatementResponseTransaction.setStatus(status);
                bankStatementResponseTransaction.setUID(UUID.randomUUID().toString());
                transactions.add(bankStatementResponseTransaction);
            }
        }

        //create response envelope
        responseMessageSet.setStatementResponses(transactions);
        responseEnvelope.setMessageSets(new TreeSet<responsemessageset>());
        responseEnvelope.getMessageSets().add(responseMessageSet);
        responseEnvelope.setSecurity(ApplicationSecurity.TYPE1);
        responseEnvelope.setUID(UUID.randomUUID().toString());
        return responseEnvelope;
    }
}
There are many classes in the library and at first the code may seem pretty incomprehensible. But you have to remember that almost all this variety of classes do nothing more than just wrap XML element. Every time you are not sure about meaning of the current operation feel free to look the library code. It is not very complicated.

By the way at first I've tried to get RequestEnvelope and send ResponseEnvelope in the web service, but I've stumbled upon implementation of ResponseEnvelope. It is dependable on some class that do not have public default constructor(UnknownStatusCode). And due to limitations of JAX-WS it was impossible to create stubs or use the common interface with the client.

That's all about the server part. In the next article we'll create the OFX client.