What is the Path SDK?

The Path SDK is a pre-built solution that simplifies building connections between our MDX specification, your application, and other third party connections using our Path framework. Path is an API framework that connects MXmobile to the MX platform and any other data source you may rely on and translates between both of these connections, despite how disparate those systems are.

Use the Path SDK to independently build connections and interchange them within your mobile application as needed. The following guides explain the Path SDK architecture in detail and provide additional instructions to build your own accessor connections, behaviors, and facilities. It is available for download from our GitHub.


Path Architecture

When you use the Path SDK, your application connects to an interface called the Path Gateway. The Gateway mirrors the MDX API protocol. The MDX protocol standardizes most common banking functions such as accounts, transfers, account origination and more. Learn more by visiting our MDX developer documentation.

Diagram explaining the information on this page

To connect the Path Gateway to your selected fintech connection, Path relies on accessors, which translate between the fintech and our MDX specification. When using the SDK, you’ll build your own accessors to accomodate these connections.

To create this connection, the Path SDK requires a configuration file named gateway.yml. This file provides the configuration which is easily modified and passed through a configurator to the Path Gateway. gateway.yml enables you to define API connections via accessors without having to change the core product associated with the Path Gateway or the MDX Interface.

In addition, the SDK also hosts facilities and behaviors. The SDK can easily switch between implementation of key/value storage, encryption appliances, and other services via facilities without having to reconfigure an accessor. It hosts behaviors in a similar way; caching, access control, and logging are all implemented within the SDK separate from the accessors.


Installing the SDK

To install the Path SDK, download it from our Github Repository.

Path SDK Github Repository

Prerequisites

To successfully use Path SDK, you’ll need the following:

  • Use of a Java Machine Language (JVM).
  • All required credentials from your source connection.

Running Path SDK Examples

We host a variety of executable examples designed to help you understand the features of the Path SDK. Each example includes a series of files that include configured gateways and accessors. Many of these files may contain in-line documentation using comments to help guide you through the code itself.

Run an example on your own to review the response of those examples, which may be referred to in our guides. The guides that follow refer to these examples through both instruction and code samples.

Download our executable examples and read more about running them at our Path SDK Examples Github Repository.


Accessors Overview

You’ll spend most of your time building accessors and configuring them. Accessors communicate and translate between the MDX models, operations, and external systems. They extend the functionality that is defined by the capablities of the MDX API.

Accessors are supported by other inputs, including gateways, scopes, connections, and behaviors. Most accessors have some additional dependencies that need to be configured to function.


Configuring Accessors

To use the Path SDK, build accessors to translate your third party connections to the MDX specification. To pull it all together, construct gateways in the gateway.yml file to connect the accessors you build. The configurations in that file are passed to a configurator and sent to the Path Gateway to achieve the connection.

Use the @Configuration annotation to pass and use values from a configurations block. The @Configuration annotation can mark a POJO constructor parameter that matches the configurations block names.

This is the preferred method because it:

  • Allows for static property names which enable the compiler to make field name changes.
  • Enables you to perform configuration-time validations for provided configurations.
  • Allows you to expose a dynamic description of the accessor’s configuration options.

In the following example, the IDaccessor class is configured using POJO.


1. Define the Gateway

In the gateway.yml file, configure the accessor as shown in the following example.

Configure ID Accessor
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  accessor:
    class: path.e02b_configuration_accessor.Accessor
    scope: prototype
    configurations:
      clientSecret: $ecr3t
      logCalls: true
    connections:
      superAuth:
        baseUrl: http://api.bank1.com/v1
        configurations:
          clientIdentifier: client1

  gateways:
    id: {}


2. Set the Configuration

In the configuration class in Example 2b, this is referred to as the AccessorConfigs class, which annotates the configuration POJO class with @ConfigurationsField.

The fields inside the configuration POJO class must be annotated with @ConfigurationField otherwise they will be ignored and not populated. The fields can be values (string, int, etc.), configuration objects, arrays, or arrays of configuration objects. All of the same rules apply to sub-configuration objects.

Code example
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package path.e02b_configuration_accessor;

import lombok.Data;

import com.mx.common.configuration.ConfigurationField;

@Data
public class AccessorConfigs {

  // Note: configuration will fail if clientSecret is not present.
  @ConfigurationField(required = true)
  private String clientSecret;

  @ConfigurationField("logCalls") // Can optionally override the expected configuration field name.
  private boolean shouldLogCalls;

}


3. Define the Accessor

In the IDAccessor.java file, configure the accessor to call the configuration you used in Step 2.

Code example
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import com.mx.accessors.AccessorConfiguration;
import com.mx.accessors.id.IdBaseAccessor;
import com.mx.common.configuration.Configuration;


public class IdAccessor extends IdBaseAccessor {

  private final AccessorConfigs configs;

  public IdAccessor(
      AccessorConfiguration configuration,
      @Configuration AccessorConfigs configs) {
    super(configuration);
    this.configs = configs;
  }
}

4. Retrieve the Configuration Values

Retrieve the configuration values within the IdAccessor class as shown below.

Code example
1
String configurationValue = configs.getKey1();

Accessor Connections

Connections and accessors work together to link and translate financial systems. A connection is required to actually make a call into a financial system that results in the translation of that information using the accessor. As with accessors, the configuration platform allows for a lot of customization when setting them up. The connection classes have a clean, fluent interface to make the calls.

In the following guides, there are two different examples that demonstrate accessor connections. Both of these examples can be cloned and executed by visiting our GitHub:

The following information demonstrates how to configure connections within an accessor.


Configuring Connections — Example 1

In the Configuring Accessors guide, you set the configuration for the IdAccessor class. In this example, you’ll create a connection class and access that connection in the IdAccessor class.

This guide references Example 2b in our GitHub.


1. Define the Gateway

An accessor can have one or more connection. In this example, define the gateway.yml with the superAuth connection you’ll build during the next few steps.

Code example
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
demo:

  accessor:
    class: path.e02b_configuration_accessor.Accessor
    scope: prototype
    configurations:
      clientSecret: $ecr3t
      logCalls: true
    connections:
      superAuth:
        baseUrl: http://api.bank1.com/v1
        configurations:
          clientIdentifier: client1

  gateways:
    id: {}

2. Configure the Connection

Configure the connection by creating a configuration class. In this example it’s referred to as the ConnectionConfig class. Similar to defining the configruation in the beginning of Example 2, use an @Configuration annotation to define the connection.

Code example
1
2
3
4
5
6
7
8
9
10
11
package path.e02b_configuration_accessor;

import lombok.Data;

import com.mx.common.configuration.ConfigurationField;

@Data
public class ConnectionConfig {
  @ConfigurationField
  private String clientIdentifier;
}

3. Create the Connection Class

After you’ve created a configuration class for the connection, access the configuration by creating a SuperAuthConnection class. Note this class uses the @Configuration annotation to mark the parameter as a connection that pulls the configuration requirements from the ConnectConfig class.

Code example
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import java.util.Objects;

import lombok.Getter;

import com.mx.common.configuration.Configuration;
import com.mx.common.connect.AccessorConnectionSettings;

public class SuperAuthConnection extends AccessorConnectionSettings {

  @Getter
  private final ConnectionConfig connectionConfig;

  public SuperAuthConnection(@Configuration ConnectionConfig connectionConfig) {
    this.connectionConfig = connectionConfig;
  }

}

4. Configure the Accessor

In the IdAccessor class, use the @Connection annotation to mark the parameter as a connection. This example configures the IdAccessor class with the SuperAuthConnection class.

Code example
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import com.mx.accessors.AccessorConfiguration;
import com.mx.accessors.id.IdBaseAccessor;
import com.mx.common.configuration.Configuration;
import com.mx.common.connect.AccessorConnectionSettings;
import com.mx.models.id.Authentication;
import com.mx.path.gateway.configuration.annotations.Connection;

import path.lib.Logger;

public class IdAccessor extends IdBaseAccessor {

  private final AccessorConfigs configs;
  private final SuperAuthConnection superAuthConnection;

  public IdAccessor(
      AccessorConfiguration configuration,
      @Configuration AccessorConfigs configs,
      @Connection("superAuth") SuperAuthConnection superAuthConnection) {
    super(configuration);
    this.configs = configs;
    this.superAuthConnection = superAuthConnection;
  }

}

Configuring Connections — Example 2

This example demonstrates configuring a connection for an AccountAccessor class with a Banking API connection. This example can be executed and tested using Example 12 in our GitHub.


1. Define the Gateway

Each accessor can be provided with one or more connections through the gateway.yml and constructor annotations.

The following is a basic configuration example in the gateway.yml.

Define the Gateway
1
2
3
4
5
6
7
gateways:
  accounts:
    accessor:
      class: com.bank.accessors.AccountAccessor
      connections:
        bank:
          baseUrl: https://dataservice.bank.com

2. Configure the Accessor

The corresponding accessor code uses an annotated constructor parameter to pull in the connection name bank. This example configures the AccountAccessor class with the BankConnection class.

Configure the Accessor
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.bank.accessors;
import com.mx.accessors.AccessorResponse;
import com.mx.accessors.account.AccountBaseAccessor;
import com.mx.models.MdxList;
import com.mx.models.account.Account;
import com.mx.path.gateway.configuration.annotations.Connection;
import com.bank.connections.BankConnection;
import com.mx.path.gateway.context.Scope;
public class AccountAccessor extends AccountBaseAccessor {
  private BankConnection bankConnection;
  public AccountAccessor(@Connection("bank") BankConnection bankConnection) {
    this.bankConnection = bankConnection;
  }
  @Override
  public AccessorResponse<MdxList<Account>> list() {
    String bankMemberId = Session.current().get(Scope.Session, "bankMemberId"); // read the bank member id from the current session
    bankConnection.getAccounts(bankMemberId);
  }
}

The BankConnection class extends the HttpAccessorConnection type as shown in the example that follows. The BankConnection class uses the request() method. The request() method is where all connections begin. This creates a configured request object that exposes a fluent interface that allows the connection code to add headers and a request body and process handlers.

In the gateway.yml file mentioned previously, the request has a baseUrl of https://dataservice.bank.com already configured.

In the request example you’ll find the following:

  • request("/mbr/accounts.srv") sets /mbr/accounts.srv as the path.
  • .withQueryParam("memberId", memberId) adds a memberId query string parameter.
  • get() executes the request with the GET http verb. In this example it executes the following request.

GET https://dataservice.bank.com/mbr/accounts.srv?memberId=M322314

BankConnection Class Example
1
2
3
4
5
6
7
8
9
10
import com.mx.path.api.connect.http.HttpAccessorConnection;
public class BankConnection extends HttpAccessorConnection {
  public List<BankAccount> getAccounts(String memberId) {
    // Connect to the bank services
    Response response = request("/mbr/accounts.srv")
        .withQueryParam("memberId", memberId)
        .get();
    // Process result and return ...
  }
}

3. Configure Request Attributes

Many financial system’s APIs require all requests to have common configurations and attributes. To avoid duplicating the code needed to add those attributes to the connection, add an override to the request() method.

In the following example, every request() result provides apiSessionKey and clientId headers. Notice that the clientId uses a static value. This likely needs to be configured and set to a different value for a test environment versus production. Like accessors, accessor connections can have a bound configuration object that provides values from the gateway.yml. All of this is defined in the Override, as shown.

Override Attributes
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import com.mx.common.collections.MultiValueMappable;
import com.mx.common.collections.SingleValueMap;
import com.mx.common.collections.SingleValueMap;
import com.mx.path.api.connect.http.HttpAccessorConnection;
import com.mx.path.gateway.context.Scope;
import com.mx.path.model.context.Session;
import java.util.Map;
public class BankConnection extends HttpAccessorConnection {
  public List<BankAccount> getAccounts(String memberId, String apiSessionKey) {
    // Connect to the bank services
    Response response = request("/mbr/accounts.srv", apiSessionKey)
        .withQueryParam("memberId", memberId)
        .get();
    // Process result and return ...
  }
  @Override
  public void request(String path, String apiSessionKey) {
    Request request = super.request(path);
    request.withHeader("clientId", "C989898");
    request.withHeader("apiSessionKey", apiSessionKey);
  }
}

4. Configure the Connection

After you’ve configured your accessor to make the connection, configure the connection in the gateway.yml. The following example demonstrates a bank connection that is attached to the accounts gateway.

The return from the request() is pre-configured with the following values that are provided in the gateway.yml :

  • baseUrl
  • certificateAlias
  • keystorePath
  • keystorePassword
  • skipHostNameVerify
bank Connection Example
1
2
3
4
5
6
7
8
9
10
11
gateways:
  accounts:
    accessor:
      class: com.bank.accessors.AccountAccessor
      connections:
        bank:
          baseUrl: https://dataservice.bank.com
          certificateAlias: bankCert
          keystorePath: ./keystore.jks
          keystorePassword: p@$$w0rd
          skipHostNameVerify: false

Read the Configuration Values

Most connections have their own configuration needs. Values like API keys or client flags may be required in a request. To determine what is required, retrieve the configuration values and read them. You can retrieve the values from a bound configuration object.
This method enables you to define the data that is provided in the configuration for this connection. You can also define key-value types and validation rules, which makes it much easier to for the configuration to discover it.

Retrieve apikey from Bound Configuration
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import com.google.gson.Gson;
import com.mx.common.configuration.Configuration;
import com.mx.common.configuration.ConfigurationField;
import com.mx.path.api.connect.http.HttpAccessorConnection;

public class ConnectionImplementation extends HttpAccessorConnection {

  public static class ConnectionImplementationConfiguration {
    @ConfigurationField(required = true)
    private String apiKey;
    
    public String getApiKey() {
      return apiKey;
    }
  }
  
  private final ConnectionImplementationConfiguration config;
  
  public ConnectionImplementation(@Configuration ConnectionImplementationConfiguration config) {
    this.config = config;
  }

  public BankAccount getAccount(String accountId, String sessionKey) {
    // Read the apiKey from bound configuration object
    String apiKey = config.getApiKey();

    return request("accountApi")
        .withHeader("apiKey", apiKey)
        .withQueryStringParam("id", accountId)
        .withProcessor((request, response) -> {
          new Gson().fromJson(response.getBody(), BankAccount.class)
        })
        .get()
        .getObject();
  }
}

After you’ve read the values, provide them in the configurations block.

apikey Example
1
2
3
4
5
6
7
8
9
gateways:
  accounts:
    accessor:
      class: com.bank.accessors.AccountAccessor
      connections:
        bank:
          baseUrl: https://dataservice.bank.com
          configurations:
            apiKey: 12345

Accessor Scopes

After you’ve constructed a gateway, you should use it to respond to API calls throughout the life of the process. The gateway is thread-safe and doesn’t store the state between calls. Because of this, use accessor scopes to indicate the instance retention policy for an accessor.

To learn more about scopes, this example can be executed using Example 3 in our GitHub. These instructions reference the file names and code samples involved in the executable example.

Scopes

The following table defines the different scopes you can assign to an accessor.

Field Use
singleton Use when one instance should handle all gateway requests.
prototype Use when a new instance should be given every time a gateway getter is called.
Scopes Example
1
2
3
accessor:
  class: path.e03_configuration_accessor_scope.Accessor
  scope: singleton

Singleton

Configure an accessor that is built to be thread-safe and doesn’t store request-level state with a singleton scope. The benefit of using the singleton scope is that the accessors don’t need to be reconstructed with every request.


Prototype

Configure an accessor that needs to be torn down and rebuilt with every request with a prototype scope. The benefit of using the prototype scope is that each call is guaranteed a to have clean slate (assuming no static classes are causing side effects). It is intended to be used for troubleshooting, backward compatibility, or in special cases.


MaxScope

The MaxScope annotation allows you to set the maximum scope that the accessor is compatible with. If you set the MaxScope to one scope and then configure an accessor in the gateway.yml for a scope that is lower in precendence, the configuration will fail.

Scope precendence is as follows:

  • Singleton — Highest
  • Prototype — Lowest

In the following example, if an accessor is developed with an internal request state, its scope must be prototype so that the scope is clean for every time the accessor is used and it won’t be used across threads. If you set the MaxScope to AccessorScope.PROTOTYPE and the accessor is ever configured with a scope of singleton, the configuration will fail.

Code example
1
2
3
4
5
6
7
8
9
10
import com.mx.accessors.AccessorConfiguration;
import com.mx.accessors.account.AccountBaseAccessor;
import com.mx.path.gateway.configuration.annotations.AccessorScope;
import com.mx.path.gateway.configuration.annotations.MaxScope;
@MaxScope(AccessorScope.SINGLETON)
public class AccountAccessor extends AccountBaseAccessor {
  public AccountAccessor(AccessorConfiguration configuration) {
    super(configuration);
  }
}

Tutorial — Building an Accessor

This tutorial demonstrates how to build and connect an accessor to the Path SDK so that it can be configured and reused. For your own practice, you can download the files for this demonstration from our GitHub. Review the README file to learn more about setting up your machine for using this practice SDK.

This tutorial uses a demo application that logs into user accounts, lists the accounts, calculates the sum of the accounts, and displays the total sum using the MDX protocol.

The Path SDK is required to connect the application to a fake bank API referred to as Hypofin to retrieve the banking information of its users. The API in this demonstration only enables user login and account listing. The application uses the Path SDK to create an accessor that translates between the Hypofin API and MDX.

At the end of this tutorial you will have:

  • Created multiple accessor classes for both ID and Accounts.
  • Built a connection to the HypoFin API that is consumed by the accessors .
  • Configured the accessors to authenticate, request information from the API, and translate it.
  • Configured the gateway.yml to connect the accessors to the Path Gateway.

1. Create the Accessor Classes

First, create three accessor classes. In this example, these are named Accessor.java, IDAccessor.java, and AccountAccessor.java. For each accessor class, extend the BaseAccessor. The BaseAccessor for each of these is defined by the MDX API. The Path SDK can implement whatever similar functionality is provided by the 3rd party data provider you are connecting to. In this example, the hypothetical fintech, Hypofin, only has ID and Accounts, so it only extends those BaseAccessors.

Accesor.java Example:

Accessor Class Example
1
2
3
4
5
public class Accessor extends BaseAccessor {
  public Accessor(AccessorConfiguration configuration) {
    super(configuration);
  }
}

IdAccessor.java Example:

Code example
1
2
3
4
5
6
public class IdAccessor extends IdBaseAccessor {

  public IdAccessor(AccessorConfiguration configuration) {
    super(configuration);
  }
}

AccountAccessor.java Example

Code example
1
2
3
4
5
public class AccountAccessor extends AccountBaseAccessor {
  public AccountAccessor(AccessorConfiguration configuration) {
    super(configuration);
  }
}

In this case, IDAccessor and AccountAccessor are considered ChildAccessors of the BaseAccessor. After you’ve created the three accessor classes navigate to the Accessor.java file. Connect each ChildAccessor for the ID and Account Accessor Classes you just created using the @ChildAccessor object.

While you are in the Accessor.java file, set the MaxScope; this determines whether the accessor should add a new instance or maintain a static instance of the accessor every time a request is made.

Code example
1
2
3
4
5
6
7
8
@MaxScope (AccessorScope.SINGLETON)
@ChildAccessor(IdAccessor.class)
@ChildAccessor(AccountAccessor.class)
public class Accessor extends BaseAccessor{ //the base accessor is the MDX accessor.
    public Accessor(AccessorConfiguration configuration) {
        super(configuration);
    }
}

2. Define the Gateway

In the gateway.yml file, modify the accessor class to the API’s root accessor.

Code example
1
2
accessor:
    class: com.mx.hypothetical.hypofin.accessor.Accessor

3. Configure the Accessor Connection


3.1. Configure the Accessor Class with the Connection

First, configure the IDAccessor class. The code example shows an annotated constructor parameter that pulls in the name hypofin. This pulls the hypofin connection from the gateway.yml and configures all of the details for the connection including the baseURL and what configuration items are needed.

Code example
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class IdAccessor extends IdBaseAccessor {

    @Getter
    private final HypoFinConnection hypoFinConnection;

    public IdAccessor(AccessorConfiguration configuration, @Connection("hypofin") HypoFinConnection hypoFinConnection) {
        super(configuration);
        this.hypoFinConnection = hypoFinConnection;
    }

    @Override
    public AccessorResponse<Authentication> authenticate(Authentication authentication) {
        getHypoFinConnection().identify(authentication.getLogin(), authentication.getPassword());
    }
}

3.2. Configure the Connection Class

Note that the code in Step 3.1 is incomplete. To move forward, you’ll need two classes, a connection class and a model class.

First create the connection class.

In this example, the connection is called HypoFinConnection. The connection class extends the accessor connection type HttpAccessorConnection. It calls the HypoIdentity model you’ll create in Step 3.3.

This configuration uses the request() method to define the connection. The following example uses the /identities endpoint as its defined in the HypoFin API. This method responds with a request object that responds with a fluent interface. This means that you can interchange calls with different headers, bodies, and change the method if required.

The request also uses the .withProcessor to deserialize the response. This request sets the body of the request and uses the POST method to execute the request.

Code example
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class HypoFinConnection extends HttpAccessorConnection {

  @Getter
  private HypofinConfiguration hypofinConfiguration;

  public HypoFinConnection(@Configuration HypofinConfiguration hypofinConfiguration) {
    this.hypofinConfiguration = hypofinConfiguration;
  }

  public HypoIdentity identify(String login, char[] password) {
    return request("/identities")
            .withBody("{\"login\":\"" + login + "\",\"password\":\"" + new String(password) + "\"}")
        .withProcessor((response) -> {
          return new Gson().fromJson(response.getBody(), HypoIdentity.class);
        })
        .post()
        .throwException()
        .getObject();
  }
}

3.3. Define the Configuration Model

Next, create the model class. The model class contains the return fields and their types of the API you’re connecting to that you’ll want to return from the connection. The connection class extends the available accessor connection type and defines the connection requirements.

First, create and configure a model class. In this example the model is named HypoIdentity.

Code example
1
2
3
4
5
@Data
public class HypoIdentity {
  private String uid;
  private String token;
}

3.4. Complete the Connection

To finish configuring the accessor connection, return to the IDAccessor class.

To complete this connection, add the HypoFinConnection and override the default authentication method. You also need to store the response using the .sput method. This will encrypt the values. This example scopes to the session, meaning that the key will be stored along with the session data. So when the session data is deleted, the value will also disappear.

Code example
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class IdAccessor extends IdBaseAccessor {

  @Getter
  private final HypoFinConnection hypoFinConnection;

  public IdAccessor(AccessorConfiguration configuration, @Connection("hypofin") HypoFinConnection hypoFinConnection) {
    super(configuration);
    this.hypoFinConnection = hypoFinConnection;
  }

  @Override
  public AccessorResponse<Authentication> authenticate(Authentication authentication) {
    HypoIdentity identity = getHypoFinConnection().identify(authentication.getLogin(), authentication.getPassword());
    Authentication response = new Authentication();
    response.setUserId(identity.getUid());

    Session.current().sput(Scope.Session, "hypoFinSessionToken", identity.getToken());

    return AccessorResponse.<Authentication>builder()
        .result(response)
        .status(AccessorResponseStatus.OK)
        .build();
  }
}

Also, add the connection to the AccountAccessor class as shown below:

Code example
1
2
3
4
5
6
7
8
9
public class AccountAccessor extends AccountBaseAccessor {
  @Getter
  private final HypoFinConnection hypoFinConnection;

  public AccountAccessor(AccessorConfiguration configuration, @Connection("hypofin") HypoFinConnection hypoFinConnection) {
    super(configuration);
    this.hypoFinConnection = hypoFinConnection;
  }
}

4. Configure the Connection in the Gateway

In gateway.yml, configure the connection. The example that follows uses a connections block to define the name of the connection hypofin as well as the base URL. It also introduces a configurations block that hosts all additional configurations required by the connection such as a client ID, API key, or token. In this case, the Hypofin API requires a clientId.

Code example
1
2
3
4
5
6
7
accessor:
    class: com.mx.hypothetical.hypofin.accessor.Accessor
    connections:
      hypofin:
        baseUrl: http://localhost:3000
        configurations:
          clientId: superfakeMobile

Because you know that the clientId is required, create a configuration class to define all required configurations. In this example, the class is named HypoFinConfiguration. In the file, use the @ConfigurationField annotation class. This class allows you to apply validations to the configuration to help troubleshoot and identify issues with the configuration early.

The following example indicates that cliendId is a required field for the Hypofin connection.

Code example
1
2
3
4
5
@Data
public class HypofinConfiguration {
  @ConfigurationField(required = true)
  private String clientId;
}

5. Apply the Configuration to the Connection

Now that you’ve defined all additional configurations, add them to the HypofinConnection class. Use a annotated paramter to review the annotation and pull the configuration block from the gateway to connect it to an instance of the configruation.

Code example
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class HypoFinConnection extends HttpAccessorConnection {
    
  @Getter
  private HypofinConfiguration hypofinConfiguration;

  public HypoFinConnection(@Configuration HypofinConfiguration hypofinConfiguration) {
    this.hypofinConfiguration = hypofinConfiguration;
  }

  public HypoIdentity identify(String login, char[] password) {
    return request("/identities")
        .withBody("{\"login\":\"" + login + "\",\"password\":\"" + new String(password) + "\"}")
        .withProcessor((response) -> {
          return new Gson().fromJson(response.getBody(), HypoIdentity.class);
        })
        .post()
        .throwException()
        .getObject();
  }
}

6. Log Out of the Connection

Throughout this process you’ve configured the accessor to login to an account and retrieve account information. You’ll need to logout using additional configuration of the HypoFinConnection Class and the IDAccessorClass.

In the HypoFinConnection class:

Code example
1
2
3
4
5
public void logout(String token) {
    request("/identities/" + token)
        .delete()
        .throwException();
  }

In the IDAccessor class add an @Override. Your final code should appear similar to the following example.

Code example
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class IdAccessor extends IdBaseAccessor {

  @Getter
  private final HypoFinConnection hypoFinConnection;

  public IdAccessor(AccessorConfiguration configuration, @Connection("hypofin") HypoFinConnection hypoFinConnection) {
    super(configuration);
    this.hypoFinConnection = hypoFinConnection;
  }

  @Override
  public AccessorResponse<Authentication> authenticate(Authentication authentication) {
    HypoIdentity identity = getHypoFinConnection().identify(authentication.getLogin(), authentication.getPassword());
    Authentication response = new Authentication();
    response.setUserId(identity.getUid());

    Session.current().sput(Scope.Session, "hypoFinSessionToken", identity.getToken());

    return AccessorResponse.<Authentication>builder()
        .result(response)
        .status(AccessorResponseStatus.OK)
        .build();
  }

  @Override
  public AccessorResponse<Void> logout(String sessionId) {
    hypoFinConnection.logout(Session.current().get(Scope.Session, "hypoFinSessionToken"));
    return AccessorResponse.<Void>builder().status(AccessorResponseStatus.NO_CONTENT).build();
  }
}

7. List the Accounts

To list accounts, inspect the API to determine what items are returned in the response. For the /account endpoint of the HypoFin API, the specification indicates that the response returns id, desc, t, and bal. Add the response items, by creating a model called HypoAccount.

Code example
1
2
3
4
5
6
7
@Data
public class HypoAccount {
  private String id;
  private String desc;
  private String t;
  private Double bal;
}

After you’ve configured the model with the response items, return to the HypoFinConnection class to import the list. Note that although the clientId is required for this request per the HypoFin API specification, it has already been defined in the gateway configurations block so it doesn’t need to be defined in the accessor.

The HypoFin API specification does require a session token which is not defined in the gateway. Therefore it must be included in the accessor.

Your final code for the HypoFinConnection class should appear as follows:

Code example
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public class HypoFinConnection extends HttpAccessorConnection {
  @Getter
  private HypofinConfiguration hypofinConfiguration;

  public HypoFinConnection(@Configuration HypofinConfiguration hypofinConfiguration) {
    this.hypofinConfiguration = hypofinConfiguration;
  }

  public HypoIdentity identify(String login, char[] password) {
    return request("/identities")
        .withBody("{\"login\":\"" + login + "\",\"password\":\"" + new String(password) + "\"}")
        .withProcessor((response) -> {
          return new Gson().fromJson(response.getBody(), HypoIdentity.class);
        })
        .post()
        .throwException()
        .getObject();
  }

  public void logout(String token) {
    request("/identities/" + token)
        .delete()
        .throwException();
  }

  public List<HypoAccount> listAccounts(String token) {
    return request("/accounts")
        .withHeader("token", token)
        .withProcessor((response) -> {
          return new Gson().fromJson(response.getBody(), new TypeToken<List<HypoAccount>>() {
          }.getType());
        })
        .get()
        .throwException()
        .getObject();
  }

  @Override
  public HttpRequest request(String path) {
    return super.request(path).withHeader("clientId", getHypofinConfiguration().getClientId());
  }
}

Next, add this to the AccountAccessor class to call the list.

Code example
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public class AccountAccessor extends AccountBaseAccessor {
  @Getter
  private final HypoFinConnection hypoFinConnection;

  public AccountAccessor(AccessorConfiguration configuration, @Connection("hypofin") HypoFinConnection hypoFinConnection) {
    super(configuration);
    this.hypoFinConnection = hypoFinConnection;
  }

  @Override
  public AccessorResponse<MdxList<Account>> list() {
    MdxList<Account> response = new MdxList<>();

    getHypoFinConnection().listAccounts(Session.current().get(Scope.Session, "hypoFinSessionToken")).forEach((hypoAccount) -> {
      Account account = new Account();
      account.setId(hypoAccount.getId());
      account.setName(hypoAccount.getDesc());
      if (hypoAccount.getBal() != null) {
        account.setBalance(BigDecimal.valueOf(hypoAccount.getBal()));
      } else {
        account.setBalance(BigDecimal.valueOf(0));
      }

      switch (hypoAccount.getT()) {
        case "CHK":
          account.setType("CHECKING");
          break;
        case "SAV":
          account.setType("SAVINGS");
          break;
        default:
          account.setType("UNKNOWN");
          break;
      }
      response.add(account);
    });

    return AccessorResponse.<MdxList<Account>>builder().result(response).status(AccessorResponseStatus.OK).build();
  }
}

Behaviors Overview

Behaviors can wrap any or all API calls into a gateway. They can be used to provide cross-cutting behaviors (i.e. caching, logging, access control, etc) or to decorate accessor functionality.

Behaviors can be chained together and executed in a specific order. The code in a behavior decides when the next behavior is called.


Behavior Configuration

As shown in the following example, behaviors are configured in the gateway.yml file similar to the accessor configuration. There is a class and optional configurations block.

Behavior Configuration
1
2
3
4
behavior:
  class: LoggingBehavior
  configurations:
    logHeader: LOG

Like accessors, the behavior class constructor can annotate a parameter with @Configuration to have a POJO bound to the values in the configurations block. In this example, a populated instance of LoggingBehaviorConfiguration is passed to the constructor. The @ClientID annotation is also supported.

Logging Behavior
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import com.mx.common.collections.ObjectMap;
import com.mx.path.gateway.behavior.GatewayBehavior;
import com.mx.path.gateway.configuration.annotations.Configuration;
import com.mx.path.gateway.configuration.annotations.ConfigurationField;
import lombok.Getter;
public class LoggingBehavior extends GatewayBehavior {
  public static class LoggingBehaviorConfiguration {
    @Getter
    @ConfigurationField("logHeader")
    private String logHeader;
  }
  
  private LoggingBehaviorConfiguration loggingBehaviorConfiguration;
  public LoggingBehavior(ObjectMap configurations, @Configuration LoggingBehaviorConfiguration loggingBehaviorConfiguration) {
    super(configurations);
    this.loggingBehaviorConfiguration = loggingBehaviorConfiguration;
  }
}

Facilities Overview

For integrations to operate within a deployment, access to infrastructure is necessary. To keep these concerns separate from accessor code, the Path SDK has built an abstraction layer for all external infrastructure concerns. These are referred to as Facilities.

Supported facilities include:

  • Encryption
  • Caching and Session Storage
  • Events (local process)
  • Messaging (pub/sub)

Facilities are set up at time of configuration and are globally accessible. Facilities can be accessed via the Facilities class in the Main.java, which is demonstrated in the example.

Facilities Class
1
Facilities.getSessionStore("client1");

Stores

A store is a simple key-value pair store. It is intended to be backed by a semi-volatile key-value database like memcached or redis. Unlike other facility types, the Store facility type is used for 2 facilities – SessionStore and CacheStore. These are used in the same way but are separated by the lifetime expectation of the data stored in them.

Many pre-built behaviors use SessionStore and CacheStore to perform their function. Accessors can use Stores directly, if needed, although it is not recommended.

  • CacheStore data can disappear at any time and should be re-constructable.
  • SessionStore should only be used to store session data with a short expiration. Assuming the session is in a good state, code that needs this data can have a reasonable expectation that it will be there when it is needed. Session uses the SessionStore internally to keep states between requests.

Store Scopes

Use session scopes to write key-value data directly to the store while maintaining appropriate key separation.

The following example wraps the SessionStore in a client-scoped Store. This is the recommended approach. The value someValue will be written for client-1 and will only retrievable by a client-scoped store configured for the same client.
This is useful if an accessor needs to store a value for a client outside the current session.
There are also Session, CurrentSession, and Global scoped-store wrappers.

Code example
1
2
3
4
5
    Store sessionStore = Facilities.getSessionStore("client-1");
    Store clientStore = ScopedStore.build(sessionStore, "client");
    clientStore.put("key1", "someValue", 600);
    
    String value = clientStore.get("key1");

Encryption Service

The encryption facility provides encrypt or decrypt operations to secure sensitive data before putting it in an insecure store or transmitting it.

Session uses the encryption service facility to encrypt the value when using sPut().

Code example
1
2
    Session.current().sput("key", "value");
    Session.current().get("key"); // will decrypt an encrypted value before returning

Event Bus

The Path system provides the event bus. Path uses the EventBus framework provided by Guava.

The event bus provides a simple publish/subscribe system to be used withing the current process. This allows for the registration of methods that will be invoked when certain events occur. The Path system published many events. Behaviors and Accessors can publish their own events as well.


Message Broker

The message broker facility is used to perform publish/subscribe style communications. For more information, review our Remote Messaging and Gateways guide.


Remote Messaging Overview

You might need to communicate with another application that is also running the Path SDK. To do so, call a remote gateway. In order to facilitate and standardize this communication, several utilities have been provided.

The following information provides configuration instructions when working with remote messaging and gateways.


Calling Remote Gateways

A RemoteAccessor class is automatically generated and provided based on the API specified in BaseAccessor. This class handles transforming requests into message payloads and transmitting them over the configured MessageBroker.

In the following example, a request is made through the configured MessageBroker and a registered responder handles the request. After response is received, you can process the accounts however you want.

Code example
1
2
3
RemoteAccessor remoteAccessor = new RemoteAccessor(); 
AccessorResponse<MdxList<Account>> response = remoteAccessor.accounts().list();
MdxList<Account> accounts = response.getResult();

Requesting a List of Accounts

Code example
1
2
3
4
5
6
7
8
// This class is thread-safe and can be reused.
RemoteAccessor remoteAccessor = new RemoteAccessor(); 

// A request is made through the configured `MessageBroker`. A registered responder will handle the request. 
AccessorResponse<MdxList<Account>> response = remoteAccessor.accounts().list();

// Once you receive a response, you can process the accounts in whatever manner you need.
MdxList<Account> accounts = response.getResult(); 

Creating a Recurring Transfer

Code example
1
2
3
4
// This class is thread-safe and can be reused.
RemoteAccessor remoteAccessor = new RemoteAccessor(); 
// Sub-accessors are accessed the same way sub-gateways are accessed.
AccessorResponse<RecurringTransfer> response = remoteAccessor.transfers().recurring().create(new RecurringTransfer());

Configuring and Exposing Remote Gateways

Gateways are, by default, not exposed to remote requests. In order to configure a gateway to respond to remote requests, you must explicitly configure each operation you want exposed in the gateway.yml file.

Consider the following configuration:

Code example
1
2
3
4
5
bigBank:
  gateways:
    accounts:
      remotes:
        list: {}

This configuration exposes the list operation on the accounts gateway for the client bigBank. This means that requests made via remoteAccessor.accounts().list() for the client bigBank will be responded to via this gateway.

This is all you need to do in order to enable a gateway to respond to remote requests. The service-to-service communication will happen over the configured MessageBroker.


Security

By default, a remote gateway will first verify that an active session exists and is in the authenticated state. If a session is not present, or is not in an authenticated state, the request is rejected and an error is returned to the caller.

In some cases, you may want to expose a gateway without requiring an authenticated session (i.e. LocationGateway#search).

If you wish to disable the authenticated session requirement, provide the following syntax for each operation.

Disabling a Required Session
1
2
3
4
5
6
7
8
bigBank:
  gateways:
    locations:
      remotes:
        get:
          requireSession: false # Disables the authenticated session requirement for `LocationGateway#get`
        search:
          requireSession: false # Disables the authenticated session requirement for `LocationGateway#search`