Brightpearl Client

A simple Java client library for the Brightpearl API

Getting Started

TL;DR If you'd like to see a complete working example, see the CodeSamples class.

Before getting started with this client library, we recommend you read the tutorials and guides on the Brightpearl website so you're familiar with how their API works.

Add the library to your project

Builds are published to the Sonatype Central Repository. If you're using Maven, add this dependency: uk.co.visalia.brightpearl:api-client:1.0.0.

The library has only two dependencies - GSON and Apache HttpComponents - to keep it as portable as possible. It can even be used on Android. GSON is used for serialising JSON requests and deserialising responses, and can be added to Maven projects as a normal dependency, and to Android projects as a library. Apache HttpComponents provides the HTTP client. You can provide your own implementation of the Client class to remove the dependency on HttpComponents - we'll provide an alternative for Android soon.

Here are the Maven dependencies you'll need for a default setup:


<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpclient</artifactId>
    <version>4.3.3</version>
</dependency>
<dependency>
    <groupId>com.google.code.gson</groupId>
    <artifactId>gson</artifactId>
    <version>2.2.4</version>
</dependency>
<dependency>
    <groupId>uk.co.visalia.brightpearl</groupId>
    <artifactId>api-client</artifactId>
    <version>1.0.0</version>
</dependency>

Create a client

For most users, the default configuration of BrighpearlApiClient will be suitable. You can configure your own HTTP client, rate limiter implementation and GSON factory if you need to - see the configuration section for details.

In the meantime, this is all you need to do to create a BrightpearlApiClient instance.


BrightpearlApiClient client = BrightpearlApiClientFactory.brightpearlApiClient().build();

Authentication

Public Apps

This section is for Brightpearl registered developers. If you're a Brightpearl customer building an app for your own account, see the next section.

We assume you know the account code and datacentre of the Brightpearl customer account you wish to connect to, and for system apps, you also have an account token for the account. Read Brightpearl's documentation to find out how to get this information.

1) Create app identity

The PublicAppIdentity object identifies your app. It can be reused for all API sessions; you only need to create it once regardless of how many Brightpearl accounts you are working with. You don't need to supply your developer secret if you will sign the account tokens yourself, or if you are only using staff authentication.

Never include your developer secret in a distributed binary e.g. a mobile or desktop app!

PublicAppIdentity appIdentity = PublicAppIdentity.create(
        "codemonkeys",
        "YmIwYWFlNjBjZGRmY2UxMw==",
        "quickinvoice");
2) Create authorisation object

There are two types of authorisation, system and staff, corresponding to Brightpearl's app types. System authorisation requires an account token which you must get from Brightpearl when the customer installs your app. Staff authentication requires a staff token, which you can request using the staff member's login details.


// Define customer account.
Account account = new Account(Datacenter.EU1, "visalia");

// SYSTEM AUTH
// Create system authentication object. As long as you included your developer
// secret in the app identity, you can supply signed or unsigned account tokens.
PublicAppAuthorisation systemAuthorisation = PublicAppAuthorisation.system(
        appIdentity,
        new Account(Datacenter.EU1, "visalia"),
        "53f5d01f-4795-40df-acc0-d15b4c8e91fc");

// STAFF AUTH
// Define staff credentials.
UserCredentials credentials = new UserCredentials("sarah@visalia.co.uk", "sesame");

// Request a staff token using provided credentials.
String staffToken = client.fetchStaffToken(
        appIdentity,
        account,
        credentials);

// Create an authentication using this token. Your developer secret is not required.
PublicAppAuthorisation appAuthorisation = PublicAppAuthorisation.staff(
        appIdentity,
        account,
        staffToken);
You can now use the authorisation object with your BrightpearlApiClient instance. See making requests for more information.

Private Apps

This section is for Brightpearl customers creating an app for their own account. If you're a registered developer, please see the section above.
1) Create app identity

The PrivateAppIdentity object identifies your app and account.


// Create your app identity (valid for all sessions).
PrivateAppIdentity appIdentity = PrivateAppIdentity.create(
        new Account(Datacenter.EU1, "visalia"),
        "visalia_quickinvoice");
2) Create authorisation object

There are two types of authorisation, system and staff, corresponding to Brightpearl's app types. System authorisation requires the account token that was generated when you created your private app. Staff authentication requires a staff token, which you can request using the staff member's login details.



// SYSTEM AUTH
// Create system authentication object for your app and account. Use the token
// shown in the Brightpearl private apps page.
PrivateAppAuthorisation appAuthorisation = PrivateAppAuthorisation.system(
        appIdentity,
        "53f5d01f-4795-40df-acc0-d15b4c8e91fc");

// STAFF AUTH
// Define staff credentials.
UserCredentials credentials = new UserCredentials("sarah@visalia.co.uk", "sesame");

// Request a staff token using these credentials.
String staffToken = client.fetchStaffToken(
        appIdentity,
        credentials);

// Create an authentication using this token.
PrivateAppAuthorisation appAuthorisation = PrivateAppAuthorisation.staff(
        appIdentity,
        staffToken);
You can now use the authorisation object with your BrightpearlApiClient instance. See making requests for more information.

Legacy authentication

Brightpearl's legacy method of authenticating API calls will be removed in the future. We recommend registering as a developer and using the new methods above.

The BrightpearlLegacyApiSession class is provided for developers still using the old method of authentication. Each instance of this class is specific to the Brightpearl account it was created for.

This class provides the same interface as BrightpearlApiClient for making API requests, and fetches auth tokens for you. By default it also handles renewal of authentication tokens automatically when they expire after a period of inactivity. Authentication exceptions will only be thrown if a new token cannot be fetched, usually because the staff member has changed their password.

The factory class will create a session instance wrapping with a default BrightpearlApiClient instance, which you can override by providing your own if you need.


Account account = new Account(Datacenter.EU1, "visalia");
UserCredentials credentials = new UserCredentials("sarah@visalia.co.uk", "sesame");
BrightpearlLegacyApiSession session = BrightpearlLegacyApiSessionFactory.newApiSessionFactory()
    .withAccount(account)
    .withUserCredentials(credentials)
    .newApiSession();

At this point, the session will not be authenticated; an authentication token will be requested when you make your first request or you can request one manually.

Making requests

You can use request builders to create requests to any resource URI, passing your own objects for serialisation in the request body and your own type references for deserialisation of the response. Here are some examples.


// Fetch a product by ID
ServiceReadRequest<List<Product>> productListRequest =
    ServiceReadRequestBuilder.<List<CustomProduct>>newGetRequest(
        ServiceName.PRODUCT,
        "/product/1007",
        new TypeToken<List<Product>>() { }.getType())
    .build();
List<Product> products = client.get(appAuthorisation, productListRequest);

// Add a new brand
Brand brand = new Brand();
brand.setName("Apple");
brand.setDescription("Consumer electronics");

ServiceWriteRequest<Integer> brandPostRequest =
    ServiceWriteRequestBuilder.newPostRequest(
        ServiceName.PRODUCT,
        "/brand",
        brand,
        Integer.class)
    .build();
Integer brandId = client.execute(appAuthorisation, brandPostRequest);

To create a request, use the static methods on ServiceReadRequestBuilder or ServiceWriteRequestBuilder supplying the service name, path and expected response type. You can supply your own instance of the ServiceName class if you are using a service not already listed.

To support generic type responses, use the GSON TypeToken class as illustrated in the example above. The builder methods that accept Type instances to define the response type cannot use the Type to infer the generic type so you must declare it.

The response type should be a class that matches the JSON response expected from Brightpearl. Alternatively, you can declare the expected response type to be GSON's JsonElement class and extract the parts of the response you need.

Field names

If you want to name a field differently in your custom object to how it appears in Brightpearl's JSON responses, use GSON's @SerializedName annotation to declare the column name for a field.


@SerializedName("name")
private String productName;

Making searches

Use the ServiceSearchRequestBuilder class to create a search request.


// Search for products by name
ServiceSearchRequest<ProductSearch> searchRequest =
    ServiceSearchRequestBuilder.newSearchRequest(
        ServiceName.PRODUCT,
        "/product-search",
        ProductSearch.class)
    .withAddedFilter("productName", "iPod")
    .build();
SearchResults<ProductSearch> results = client.search(appAuthorisation, searchRequest);

Field names

Fields in each result array are matched to the result class using the column name index included in each search response. For example, if column 0 is named productId, the value at index 0 of each result array will be copied to the productId field of a result object.

If the column name is not suitable for a Java field name, use GSON's @SerializedName annotation to declare the column name for a field.


@SerializedName("ISBN")
private String isbn;

Reference data extraction

Reference data is included in some search APIs to provide additional data for search results, either to avoid repeating the same data on every result, or to support the inclusion of complex types that cannot be included in result arrays. For example, the product search response includes a list of category IDs for each product.

This library supports automatic copying of reference data into search results, by looking up the key contained in each result array from the corresponding reference data map. To use this feature in your own custom types, annotate the fields with ReferenceKey and ReferenceField.

We'll use a cut-down response from the goods in note search API to illustrate this. This response includes a productId column which can be used to look up product names, and a warehouseId column from which we can get the warehouse name.


{ response: {
    metaData: {
        columns: [
            { name: "productId" },
            { name: "warehouseId" }
        ]
    },
    results: [
        [1008, 2],
        [1015, 3]
    ]
}, reference: {
    warehouseNames: {
        2: "Main warehouse",
        3: "Edinburgh warehouse"
    },
    productNames: {
        1008: "PPL Book 1: Air Law",
        1015: "David Clark H10-13.4 Headset"
    }
}}

In our custom type, we annotate the two ID fields with @ReferenceKey to indicate they are keys to reference data maps, and the two name fields with @ReferenceField to indicate they should receive value from the reference data.


public class GoodsInNoteSearch {

    @ReferenceKey("productNames")
    private Integer productId;

    @ReferenceKey("warehouseNames")
    private Integer warehouseId;

    @ReferenceField("productNames")
    private String productName;

    @ReferenceField("warehouseNames")
    private String warehouseName;

    // Getters and setters

 }

The @ReferenceKey annotation accepts an array of reference data map names so that the same field can be used as a key for more than one map.

Multimessages

To fully understand this page we recommend you read Brightpearl's introduction to the multi-message API.

Introduction

Using the multi-message API reduces latency on slow connections, and each multi-message counts as only one request against the request cap, regardless of how many individual messages are included. This library provides full support for creating multi-messages, and a simple method for checking the results.

Multi-message requests are built by adding individual requests to a MultiRequestBuilder instance. Each individual request (or message) is a POST, PUT or DELETE request created using the same request builder classes used to send a request directly. The response from Brightpearl is returned as a MultiResponse, from which responses to individual requests can be extracted.

This is a simple example with no error checking.


ServiceWriteRequest request1 = ServiceWriteRequestBuilder
        .newPostRequest(ServiceName.PRODUCT, "/brand", brand1, Integer.class);
ServiceWriteRequest request2 = ServiceWriteRequestBuilder
        .newPostRequest(ServiceName.PRODUCT, "/brand", brand2, Integer.class);

MultiRequestBuilder request = MultiRequestBuilder.newMultiRequest()
        .withAddedRequest(request1)
        .withAddedRequest(request2);

MultiResponse multiResponse = client.execute(appAuthorisation, request);
Integer brandId1 = multiResponse.getServiceResponse(request1).getResponse();
Integer brandId2 = multiResponse.getServiceResponse(request2).getResponse();

Error handling

If a request to the multi-message API fails with an HTTP transport error, the method will throw a BrightpearlHttpException. This is not a guarantee that the individual requests have not been executed because the error may have occurred after Brightpearl accepted the batch for processing.

When an error status is received from the Brightpearl multi-message API, this indicates none of the requests have been processed, and an exception from the uk.co.visalia.brightpearl.apiclient.exception package will be thrown.

If Brightpearl returns a 200 or 207 response, the response is parsed into a MultiResponse, containing a list of the RUIDs of requests that Brightpearl did not process, and individual responses (including the status code and any error messages) for each request that was processed.

The following code is an example of multi-message handling with error checking.


ServiceWriteRequest request1 = ServiceWriteRequestBuilder
        .newPostRequest(ServiceName.PRODUCT, "/brand", brand1, Integer.class);
ServiceWriteRequest request2 = ServiceWriteRequestBuilder
        .newPostRequest(ServiceName.PRODUCT, "/brand", brand2, Integer.class);

MultiRequestBuilder request = MultiRequestBuilder.newMultiRequest()
        .withAddedRequest(request1)
        .withAddedRequest(request2)
        .withOnFailOption(OnFailOption.CONTINUE)
        .withProcessingMode(ProcessingMode.PARALLEL);

try {
    MultiResponse multiResponse = session.execute(request);

    if (!multiResponse.getUnprocessedRequestRuids().isEmpty()) {
        // Handle messages that were not processed
    }

    ServiceResponse<Integer> response1 = multiResponse.getServiceResponse(request1);
    if (response1 == null) {
        // Brand 1 request was not processed
    } else if (response1.isSuccess()) {
        // Brand 1 was created
        Integer brand1Id = response1.getResponse();
    } else {
        // Brand 1 request failed - in most cases a collection of errors will give more details
        List<ServiceError> brand1Errors = response1.getErrors();

        // Calling getResponse() when isSuccess() returns true will throw the exception received
        try {
            response1.getResponse();
        } catch (BrightpearlClientException e) {
            // Handle response failure
        }
    }
} catch (BrightpearlHttpException e) {
    // Transport error
} catch (BrightpearlClientException e) {
    // An exception corresponding to the response code returned by BP
}

Large batches

Brightpearl's multi-message API supports between 2 and 10 messages per batch. To simplify the use of this API, the multi-message methods in this library can handle any number of requests and will split them into batches of 10 or send individual requests direct to the target API.

This involves returning a simulated MultiResponse that looks similar to how Brightpearl's response would have looked if the requests could be sent as one batch. When HTTP transport errors occur, it isn't possible to do this accurately, so you may prefer to manually batch your requests.

Error handling

The get(), search() and execute() methods of BrightpearlApiSession and BrightpearlApiClient only return normally if they successfully executed the request and parsed the result into the expected type. For all error conditions, subclasses of BrightpearlClientException are thrown.

Some of these exceptions are handled slightly differently for multi-message requests. Please see the multi-message guide and the API documentation for BrightpearlApiClient for details.

BrightpearlHttpException
Indicates an HTTP transport error during the request to Brightpearl or an unexpected response format - either an empty body, badly formatted JSON, or JSON that does not match the expected response type declared by the request. The clientErrorCode field provides the reason. Note that if you provide your own implementation of Client, it should throw this exception in the event of an HTTP error. This exception is NOT used when any non-zero response code is received from the API - see BrightpearlServiceException.
BrightpearlUnavailableException
When a 503 response is received from Brightpearl, but the response body does not indicate the request cap has been reached, this exception is thrown. It indicates the account is suspended or temporarily unavailable, or Brightpearl is performing maintenance.
BrightpearlRequestCapException
Used when a 503 response is received from Brightpearl with a response body that indicates the request cap was reached. The request may be retried later.
BrightpearlServiceException
Thrown when the Brightpearl API returns a collection of error messages in the response, regardless of the status code. The status code and error messages are included in the exception.
BrightpearlAuthException
Thrown when Brightpearl returns a 401 response, indicating the auth token used is not valid. If you are using BrightpearlApiSession with automatic re-authentication enabled, it will catch this exception and attempt to re-authenticate, but it can still throw this exception if the attempt fails.
BrightpearlClientException
The superclass exception is used for errors that have occurred within the client library itself, and not as a result of a response from Brightpearl. For example, automatic re-authentication blocks execution of requests by other threads, and a timeout in those threads waiting for the token will cause this exception.

Rate limiting

Brightpearl allows 200 API requests per minute per account. Once this limit is reached, Brightpearl returns 503 responses with a specific message in the body, and this library catches this error and throws a BrightpearlRequestCapException. It is guaranteed that the request has not been executed by Brightpearl under these circumstances, so you can wait and repeat it later.

In applications that expect constant high load or short bursts of intense activity, it may be preferable to actively avoid reaching the request cap rather than routinely catching the request cap exception and attempting recovery.

Efficiency

Many applications will not need to make 200 API requests per minute, and single threaded applications may not achieve this rate. For applications that do need to make many requests to single Brightpearl accounts, it may be possible to avoid reaching the request cap by using requests efficiently.

In summary, the techniques for avoiding the request cap are:

  • Use ID sets on GET requests to fetch many objects at once
  • Cache GET responses
  • Batch POST, PUT and DELETE requests using the multi-message API

Client side throttling

To help you avoid reaching the request cap, we have provided the RateLimiter interface. The implementation of this interface will be called before every request is sent to Brightpearl, when each response is received containing the headers brightpearl-requests-remaining and brightpearl-next-throttle-period, and whenever a request cap error is received.

We have provided two implementations of this interface. NoOpRateLimiter does nothing in any method, and allows an unlimited rate of requests. ConstantWaitRateLimiter ensures that no two requests are sent within 300ms of each other, so that no more than 200 can be sent within one minute. The latter is quite basic and not suited to applications that may need to send bursts of fewer than 200 requests in well under one minute, as it will unnecessarily delay them. In an application with hundreds of threads attempting to send requests at well over 200 per minute, it may block some threads for excessively long periods.

You can use your own implementation of RateLimiter to throttle requests using the strategy of your choice, or simply reject requests by throwing a RuntimeException. See the configuration section for details.

Distributed environments

If multiple servers will be simutaneously making requests to a single Brightpearl account, the request cap for that account is shared between them. Although ConstantWaitRateLimiter can be configured with a longer minimum period between requests to allow each server one half or one third of the request cap, this approach is not recommended.

A custom implementation of RateLimiter could be used to share state between the servers (for example, via Hazelcast) to ensure the request cap is not reached.

Further reading

Configuration

Defaults are available for all the configuration options exception for account and authentication details. These defaults are:

  • HTTP client implementation using Apache HttpClient 4, with a PoolingClientConnectionManager allowing 10 concurrent connections per route and 20 total. See the contant fields section in the API docs for HttpClient4ClientFactory.
  • GSON configured with a type adapter for java.util.Calendar. Calendar is used in favour of java.util.Date for its timezone support.
  • No rate limiting or request cap error recovery.
  • No response caching.
  • Automatic re-authentication disabled in BrightpearlApiSession instances.

Configuration options

BrightpearlAPIClientFactory
clientFactory Configures the client with a custom HTTP client factory implementation that implements the ClientFactory interface. This may be the included HttpClient4ClientFactory with your preferred connection settings, or an entirely different implementation using another HTTP library. See below for configuration options of the HttpClient4 implementation.
rateLimiter By default no rate limiting is applied to requests, and you may reach the request cap of 200 per minute during busy periods. An implementation of RateLimiter may be provided to delay requests under high load, to avoid reaching the limit. The included implementation ConstantWaitRateLimiter ensures a minimum time between requests (300ms).
gson The default configuration of GSON includes support for serialising and deserialising java.util.Calendar instances, as used by the domain objects included with this library. If you are using your own domain objects, for example with Joda dates, use this option to provide a GSON instance configured appropriately.
BrightpearlLegacyApiSessionFactory (Legacy authentication)
account Provide an instance of Account containing the datacenter and an account code of a Brightpearl customer account. The datacenter can be found on Brightpearl's URI Syntax page. Required for all sessions.
userCredentials Provide an instance of UserCredentials with the email address and password of an API-enabled user account. Required when creating an API session that is able to generate an auth token for itself. If you are generating auth tokens externally to create pre-authenticated sessions this is not required.
expiredAuthTokenStrategy Configures the session to either throw all expired auth token exceptions, or to catch these exceptions and attempt to use the configured credentials to create a new auth token. Enum values are FAIL and REAUTHENTICATE. The REAUTHENTICATE option cannot be used with sessions created using a pre-fetched token.
brightpearlApiClient Sessions are account-specific wrappers for a BrightpearlApiClient instance. This method allows you to provide a client instance with custom configuration, as described above.
HttpClient4ClientFactoryBuilder
connectionManagerTimeoutMs Maximum time in milliseconds to wait for a pooled connection to become free.
connectionTimeoutMs Determines the timeout in milliseconds until a connection is established. A timeout value of zero is interpreted as an infinite timeout.
socketTimeoutMs Defines the socket timeout in milliseconds, a maximum period of inactivity between two consecutive data packets. A timeout value of zero is interpreted as an infinite timeout.
allowRedirects Defines whether redirects should be followed. Redirects are not expected from the Brightpearl API, and are not followed by default.
maxConnections Defines the maximum number of connections allowed for all routes.
maxConnectionsPerRoute Defines the maximum number of connections allowed per route (target server, or Brightpearl datacenter). This is not a limit per Brightpearl account.

Example configuration

Here's an example of how to configure a client with customised HTTP client settings and GSON configuration, and a request rate limiter.


ClientFactory clientFactory = HttpClient4ClientFactoryBuilder
        .httpClient4ClientFactory()
        .withConnectionManagerTimeoutMs(10000)
        .withConnectionTimeoutMs(15000)
        .withSocketTimeoutMs(5000)
        .withAllowRedirects(false)
        .withMaxConnections(60)
        .withMaxConnectionsPerRoute(20)
        .build();

BrightpearlApiClient client = BrightpearlApiClientFactory
        .brightpearlApiClient()
        .withClientFactory(clientFactory)
        .withRateLimiter(new ConstantWaitRateLimiter(100, 1, TimeUnit.MINUTES))
        .withGson(new GsonBuilder().create())
        .build();

// LEGACY AUTH ONLY
BrightpearlLegacyApiSession session = BrightpearlLegacyApiSessionFactory
        .newApiSessionFactory()
        .withBrightpearlApiClient(client)
        .withExpiredAuthTokenStrategy(ExpiredAuthTokenStrategy.FAIL)
        .withAccount(new Account(Datacenter.EU1, "visalia"))
        .withUserCredentials(new UserCredentials("sarah@visalia.co.uk", "sesame"))
        .newApiSession();

Version history

Version Date Notes
v1.0.0 2014-05-28 Initial release

License

This library is copyright 2014 David Morrissey, and licensed under the Apache License, Version 2.0.