Variant Java Client

Variant Experience ServerResourcesDocumentation0.9Clients ⟫ Java

Variant Java Client, User Guide

Release 0.9, June 2018

Related documentation: Variant API JavaDoc.

1Overview

Variant Java client is a library which enables the host application to communicate with Variant Experience server. It exposes server’s functionality in a native Java API and in terms of native Java classes. It can be consumed by any host application, written in Java or another JVM language. It requires Java runtime 8 or higher.

Variant Java client is consumable by any Java program because it makes no assumptions about the technology stack of the host application. This flexibility comes at the expense of some deferred dependencies, such as a mechanism to track Variant session ID between state request. These deferred dependencies are left for the application developer to provide at runtime.

Most Java Web applications are written on top of the Servlet API. These applications should take advantage of the servlet adapter for Variant Java client. It re-writes deferred method signatures in terms of familiar servlet objects, like HttpServletRequest and provides servlet-based implementation of the session ID tracker.

2Variant Java Client

Variant Java client is a library which enables the host application to communicate with Variant Experience server. It exposes server’s functionality in a native Java API and in terms of native Java classes. It has minimal external dependencies (discussed in the next section) and can be consumed by any host application, written in Java or another JVM language. It requires Java runtime 8 or higher.

2.1Installation

Download Variant Java client distribution.

Unpack the distribution:

% unzip /path/to/variant-java-<release>.zip

This will inflate the following artifacts:

File Description
variant-java-client-<release>.jar Variant Java client library. Must be present on the host application’s classpath.
variant.conf Sample configuration file containing all default configuration settings. To override any of the defaults, change their values in this file and place it on the host application’s classpath.
variant-core-<release>.jar Dependent Variant core library. Must be present on the host application’s classpath.

If the host Java application is built using a dependency management tool like Maven, you need to install the two jars above into your local repository (replacing <release> with the particular version number you’re installing, e.g. 0.9.0.)

% mvn install:install-file -Dfile=/path/to/variant-java-client-<release>.jar -DgroupId=com.variant -DartifactId=java-client -Dversion=<release> -Dpackaging=jar

% mvn install:install-file -Dfile=/path/to/variant-core-<release>.jar -DgroupId=com.variant -DartifactId=variant-core -Dversion=<release> -Dpackaging=jar

and add the corresponding dependencies to your host application’s pom.xml:

<dependency>
   <groupId>com.variant</groupId>
   <artifactId>java-client</artifactId>
   <version>[0.9,)</version>
</dependency>

<dependency>
   <groupId>com.variant</groupId>
   <artifactId>variant-core</artifactId>
   <version>[0.9,)</version>
</dependency>

Variant Java client has a small set of external transitive dependencies, which are not included in the distribution:

If these dependencies aren’t already used by your host application, add them to your application’s pom.xml file:

<dependency>
   <groupId>org.apache.httpcomponents</groupId>
   <artifactId>httpclient</artifactId>
   <version>4.5.1</version>
</dependency>

<dependency>
   <groupId>com.typesafe</groupId>
   <artifactId>config</artifactId>
   <version>1.2.1</version>
</dependency>

<dependency>
   <groupId>org.apache.commons</groupId>
   <artifactId>commons-lang3</artifactId>
   <version>3.4</version>
</dependency>

<dependency>
   <groupId>org.slf4j</groupId>
   <artifactId>slf4j-api</artifactId>
   <version>1.7.12</version>
</dependency>

If you are not using a dependency management tool, you will need to download these as JARs from their respective project portals and add them to the host application’s classpath manually.

2.2Configuration

Variant client is configured via configuration properties, organized in conf files. Variant uses the Lightbend Config library , an implementation of the HOCON configuration grammar.

At instantiation, Variant client searches the runtime class path for the file variant.conf. If it is found, its contents override the defaults. The sample variant.conf file, included in the distribution, lists all the default values. If you need to override any of the properties, edit this file and add it to your application’s runtime classpath.

An alternate config file can be provided on the command line in either of these Java system variables, either on the command line or programmatically:

Java System Variable Description
variant.config.file Specifies alternate config file as a file system file, e.g. on command line:
-Dvariant.config.file=/path/to/alt/config/as/file. Must exist.
variant.config.resource Specifies alternate config file as a classpath resource, e.g. on command line:
-Dvariant.config.resource=/path/to/alt/config/as/resource. Must exist.

It is an error to set both variant.config.file and variant.config.resource system properties.

Finally, each individual config parameter may be overridden from the command line via the JVM system variable of the same name, e.g. -Dvariant.server.url=http://variant.mydomain.com:5377/variant.

All config properties recognized by Variant Java client are aliased to constants in the ConfigKeys  interface and are explained in the following table.

Property Default Value / Description
session.id.tracker.class.name "no default"
Session ID tracker implementation. Must be supplied by the user. See Section 2.4.2 for details.
server.url "http://localhost:5377/variant"
Variant server URL.

2.3Typical Usage Example

Host application obtains an instance of VariantClient  from the factory method:

VariantClient client = VariantClient.Factory.getInstance();

The host application should hold on to the client object and reused for the life of the JVM.

Connect to a variation schema on Variant server:

Connection connection = client.connectTo("myschema");

The host application should hold on to the connection object and reuse it for all user sessions. Variant connections are stateless, so they are reusable even after a server restart.

Obtain (or create) a Variant session. They are completely distinct from your host application’s sessions.

// userData is a depends on the environment.
Session session = connection.getOrCreateSession(userData);

The userData argument is a deferred dependency, as discussed in the next section.

Obtain the schema and the state.

Schema schema = session.getSchema();
Optional<State> loginPage = schema.getState("loginPage");
if (!loginPage.isPresent()) {
   System.out.println("State loginPage is not in the schema. Falling back to control.");
}

Obtain the state request and figure out the live experience(s) the session is targeted for.

StateRequest request = session.targetForState(loginPage.get());
request.getLiveExperiences().forEach(e ->
   System.out.println(
      String.format(
         "We're targeted to experience %s in variation %s", 
         e.getName(), 
         e.getVariation().getName()))
);

At this point the application can do what’s needed for the particular experience(s) this user session has been targeted for. After the host application’s code path is complete, the state request must be committed (if no exceptions were encountered) or failed (if something went awry).

request.commit(userData);  // or .fail(userData)

Here again the userData argument is a deferred dependency and its meaning is explained in the next section. Committing or failing a state request both implicitly trigger the associated state visited trace event with the corresponding completion status.

2.4Deferred Dependencies

Variant Java client makes no assumptions about host application technology or operational details. This generality enables broad applicability: any JVM host application can use it to access Variant server. The flip side of this generality is that Variant Java client must rely on a few elements to be provided at runtime by the application developer, collectively known as deferred dependencies.

2.4.1Session ID Tracker

Variant maintains its own sessions, independent from those maintained by the host application. Variant server creates and maintains these sessions, but the client must provide a way of relating two consecutive state requests to the same session. Session ID tracker does exactly that. The session state is kept on Variant server, but the host application is responsible for holding on to the session ID, by which this state can be retrieved.

To fulfill this responsibility, the application developer must supply an implementation of the SessionIdTracker  interface, which must provide a public nullary constructor and implement the following public methods:

void init(Object...userData)
Called by Variant to initialize a newly instantiated implementation immediately, within the scope of the Connection.getSession() methods. The meaning of userData is strictly up to the implementation: Variant will pass the arguments to the enclosing call to Connection.getSession(Object...userData) or Connection.getOrCreateSession(Object...userData) into this method without interpretation.
String get()
Retrieves the current value of the session ID from this tracker.
void set(String sessionId)
Sets the value of session ID.
void save(Object...userData)
Saves the currently held session ID to the underlying persistence mechanism. The meaning of userData is strictly up to the implementation: Variant will pass the arguments to the enclosing call to StateRequest.commit(Object...userData) or StateRequest.fail(Object...userData) into this method without interpretation.

The implementing class must be placed on the host application’s classpath and configured via the session.id.tracker.class.name config parameter. For a sample implementation, see Section 3.4.

2.4.2Method Signatures with Deferred Parameters

The following table lists all the methods in Variant Java client whit environment dependent signatures.

Method Explanation
Connection.getOrCreateSession()  Get, if exists, or create, if does not exist, the Variant session with the externally tracked ID. The arguments are passed, without interpretation, to the underlying session ID tracker’s init()  method.
Connection.getSession()  Get existing Variant session with the externally tracked ID. The arguments are passed, without interpretation, to the underlying session ID tracker’s init()  method.
StateRequest.commit()  Commit this state request. The arguments are passed, without interpretation, to the underlying session ID tracker’s save()  method.
StateRequest.fail()  Fail this state request. The arguments are passed, without interpretation, to the underlying session ID tracker’s save()  method.

3Java Servlet Adapter

3.1Overview and Installation

Most Java Web applications are written on top of the Servlet API, either directly or via a servlet-based framework, such as Struts or Spring. Such applications should take advantage of the servlet adapter to Variant Java client. The servlet adapter is an open source project available directly on GitHub .

The servlet adapter consists of the following three components:

Component Explanation
Wrapper Servlet API  Wraps the generic Java client API with a higher interface, which re-writes all deferred method signatures in terms of familiar servlet objects, like HttpServletRequest. See Section 3.2 for further details.
SessionIdTrackerHttpCookie  Servlet-based implementation of the session ID tracker based on HTTP cookies. See next section for configuration information. See Section 3.3 for further details.
VariantFilter  Optional convenience class. Implements javax.servlet.Filter and enables eager instrumentation of Variant variations based on server-side forwards. See Section 3.4 for further details.

To install the servlet adapter for Variant Java client, follow the installation instructions on GitHub.

3.2Servlet Adapter Wrapper API

Java web applications, which are built on top of the servlet API, should communicate with Variant server via the VariantFilterservlet adapter API ‘s com.variant.client.servlet.* classes, instead of the general purpose API’s com.variant.client.* classes. The servlet adapter classes wrap the general purpose classes in a congruent API whose only difference is that it rewrites all deferred environment-dependent method signatures with those that operate on the familiar servlet objects, e.g. HttpServletRequest and HttpServletResponse.

Here’s the typical usage example from Section 2.3, re-written in terms of the servlet adapter API:

Host application obtains an instance of ServletVariantClient  from the factory method:

ServletVariantClient client = ServletVariantClient.Factory.getInstance();

The host application should hold on to the client object and reused for the life of the JVM.

Connect to a variation schema on Variant server:

ServletConnection connection = client.connectTo("myschema");

The host application should hold on to the connection object and reuse it for all user sessions. Variant connections are stateless, so they are reusable even after a server restart.

Obtain (or create) a Variant session. They are completely distinct from your host application’s sessions.

// request is the current HttpServletRequest
ServletSession session = connection.getOrCreateSession(request);

Obtain the schema and the state.

Schema schema = session.getSchema();
Optional<State> loginPage = schema.getState("loginPage");
if (!loginPage.isPresent()) {
   System.out.println("State loginPage is not in the schema. Falling back to control.");
}

Obtain the state request and figure out the live experience(s) the session is targeted for.

ServletStateRequest request = session.targetForState(loginPage.get());
request.getLiveExperiences().forEach(e ->
   System.out.println(
      String.format(
         "We're targeted to experience %s in variation %s", 
         e.getName(), 
         e.getVariation().getName()))
);

At this point the application can do what’s needed for the particular experience(s) this user session has been targeted for. After the host application’s code path is complete, the state request must be committed (if no exceptions were encountered) or failed (if something went awry).

// response is the HttpServletResponse
request.commit(response);  // or .fail(response)

Committing or failing a state request both implicitly trigger the associated state visited trace event with the corresponding completion status.

3.3SessionIdTrackerHttpCookie

Servlet adapter for Variant Java client comes with a concrete implementation  of the SessionIdTracker interface . It uses the HTTP cookie mechanism to track Variant session ID between state requests in the session-scoped cookie named variant-ssnid, much like servlet containers use the JSESSIONID cookie to track the HTTP session ID. You must configure this implementation in your application’s the variant.conf file as follows:

session.id.tracker.class.name = "com.variant.client.servlet.SessionIdTrackerHttpCookie"

3.4VariantFilter

VariantFilter is a utility class, intended to fast-track a common special case when you can map your schema states to the host Web application’s resource paths. For example, consider a variation where you add Recaptcha to your application’s signup page, currently mapped to /signup. It’s natural to map the implementation of the new page to its own path /signup-recap. (The back-end implementation may, and probably should share code with the existing implementation, of course.) Then, routing user traffic between the two pages becomes a matter of server-side forward: if the incoming request for /signup, then, depending on the targeting, we either let it right through, or forward to /signup-recap.

In fact, instrumenting this sample variation with Variant takes no code whatsoever. All you have to do is 1) create the variation schema:

{
   'meta':{
      'name':'VariantFilterDemo',
      'comment':'Demonstrates the use of VariantFilter eager instrumentation'
   },
   'states':[                                                
      { 
         'name':'signup', 
         'parameters': [
            {'name':'path', 'value':'/signup'}
         ]
      }
   ], 
   'variations':[
      {
         'name':'SignupPageRecaptcha',
         'experiences':[                                     
            {'name':'noRecaptcha', 'isControl':true},                                               
            {'name':'withRecaptcha'}              
         ],                    
         'onStates':[
            {
               'stateRef':'signup',                    
               'variants':[                                  
                  {                                          
                     'experienceRef': 'withRecaptcha',
                     'parameters': [
                        // Override base parameter 'path'. Sessions targeted to this experience
                        // will be forwarded to this new path automatically by VariantFilter.
                        {'name':'path', 'value':'/signup-recap'}
                     ]
                  }                                          
               ]                                             
            }
         ]
      }
   ]
}

…and 2) configure VariantFilter  in your application’s web.xml file:

...
<filter>
   <filter-name>variantFilter</filter-name>
   <filter-class>com.variant.client.servlet.VariantFilter</filter-class> 
   <init-param>
      <param-name>schema</param-name>
      <param-value>VariantFilterDemo</param-value>
   </init-param>
</filter>

<filter-mapping>
   <filter-name>variantFilter</filter-name>
   <url-pattern>/*</url-pattern>
</filter-mapping>
...

The init parameter schema is the name of the variation schema, created in step 1). VariantFilter connects to it at application startup, and, as your application receives and serves HTTP requests, VariantFilter intercepts all incoming requests and matches their paths against the value given in the state’s base path parameter value /signup. All matched requests are targeted for the signup state. If the session gets targeted to the control (noRecaptcha) experience, VariantFilter lets the request fall through, continuing on its way to the /signup resource. If the session gets targeted to the withRecaptcha experience, VariantFilter redirects to /signup-recap.

In real life, the same server entry point may be mapped to the same server entry point. VariantFilter supports regular-expression wildcards as documented in StateSelectorByRequestPath  utility class. Here’s a few examples:

{
   ...
   'parameters': [
      {
         'name':'path',
         'value':'/user/details/~\\d+/'
      }
   ]
   ...   
} 

Here, the string expression /user/details/~\\d+/ will match any string with the prefix '/petclinic/owners/', followed by one or more digits, followed by the closing slash.

More formally, the StateSelectorByRequestPath path matcher interprets the expression in the listing above as follows:

  1. Symbol ‘/’ always stands for the path separator. Any path must start with ‘/’.
  2. Any sequence of symbols between two consecutive symbols ‘/’ is taken to be a literal, unless starts with the a tilde ‘~’, in which case the string immediately following the tilde and ending with, but not including, the next unescaped ‘/’ is considered a regular expression. Complete regular expression syntax  is supported.
  3. Although symbol ‘/’ does not have a special meaning in the regular expression grammar, it does to Variant: this is how Variant decides where a regular expression ends. Therefore, if ‘/’ must be included in the regular expression, it must be escaped with the ‘\’ symbol, like any other special character. By including ‘/’ symbols in the regular expression, it is possible to match variable sections in the middle of a path.
  4. Symbol ‘//’ can be used anywhere, where ‘/’ can be used, and is a shortcut for ‘/~.*/’. In other words, ‘//’ will match any string surrounded by slashes. Thus, that ‘///’ is legal but superfluous.
  5. The very last ‘/’ of the pattern is not significant, i.e. Variant will remove it, if present, from both the pattern and the path after all ‘//’ are expanded. This enables easy prefix match: ‘/user//’ will match any path that starts with ‘/user/’.

Examples:

Path Will Match Will not match
/user /user
/user/
/user/new
/user// /user
/user/
/user/new
/service/user/
/user//.html /user/new/error.html /user/error