3 ApiFest Mapping Server

Now let's see how the ApiFest Mapping Server could be started and what configurations are needed in order to translates the requests between the public API and your backend API.

3.1 Start ApiFest Mapping Server

In order to start the ApiFest Mapping Server, you should configure several properties. Here is how the configuration file looks like:
apifest.host=
apifest.port=
apifest.mappings=
apifest.global-errors=
token.validate.host=
token.validate.port=
connect.timeout=
custom.jar=
apifest.nodes=
hazelcast.password=
In the following table you can find the description of each property, whether it's mandatory and an example value.
property description required example
apifest.host The host on which the ApiFest Mapping Server will run. false, default value localhost apifest.host=127.0.0.1
apifest.port The port on which the ApiFest Mapping Server will run. false, default value 8181 apifest.port=8181
apifest.mappings The path to the folder where mappings configuration files are placed. The path to the folder where mappings configuration files are placed. apifest.mappings=/home/apifest/mappings
apifest.global-errors The path to the XML file that describes custom global errors. false apifest.global-errors=/home/apifest/global-errors.xml
token.validate.host The host where the ApiFest OAuth 2.0 Server (or a balancer in front of the ApiFest OAuth 2.0 Server nodes) runs and access tokens will be validated. false**, no access token validation will be performed token.validate.host=127.0.0.1
token.validate.port The port where the ApiFest OAuth 2.0 Server (or a balancer in front of the ApiFest OAuth 2.0 Server instances) runs and access tokens will be validated. false**, no access token validation will be performed token.validate.port=8181
connect.timeout The connection timeout (in ms) used for connections to the backend application. false, default value=10 connect.timeout=5
custom.jar The path to the jar with your custom classes that implement request/response transformations (actions/filters). false custom.jar=/home/apifest/transformations.jar
apifest.nodes A comma-separated list of all ApiFest Mapping Server nodes defined by host only. Used to create a Hazelcast cluster that will store the loaded mapping configuration. false, if not set only localhost will be used apifest.nodes=10.32.23.11, 10.32.23.10
hazelcast.password Hazelcast password used to secure the Hazelcast nodes. If not defined the default Hazelcast password will be used. false, default Hazelcast password will be used - dev-pass hazelcast.password=securepass
* If apifest.mappings property is not set, then a warning in the log will appear on the server startup.
** If token.validation.host and token.validation.port are not defined then the access token could not be validated and the response will be HTTP 401 {"error":"access token not valid"}. Warnings in the log will appear that these two properties are not set. Also, if the ApiFest Mapping Server could not connect to the ApiFest OAuth 2.0 Server, for any reason, the same error response will be returned. The corresponding error will be visible in the server log.

The properties file should be passed as an environment variable on server startup:
-Dproperties.file=/home/apifest/apifest.properties

Once, the server is started, the following message will appear in the log:
ApiFest Mapping Server started at [host:port]

Log4j configuration
By default, the ApiFest Mapping Server starts with a log4j.xml configuration that will log in the console. In order to customize the log configuration, use standard lo4j.xml configuration and pass it as a system property on the ApiFest Mapping Server startup.

Here is an example for log4j.xml:
<!DOCTYPE log4j:configuration SYSTEM "log4j.dtd">
<log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/">
    <appender name="Console" class="org.apache.log4j.ConsoleAppender">
        <layout class="org.apache.log4j.PatternLayout">
            <param name="ConversionPattern" value="%d %-5p [%c] %m%n" />
        </layout>
    </appender>
    <appender name="File" class="org.apache.log4j.DailyRollingFileAppender">
        <param name="File" value="apifest-mapping.log" />
        <param name="Append" value="true" />
        <param name="DatePattern" value="'.'yyyy-MM-dd" />
        <layout class="org.apache.log4j.PatternLayout">
            <param name="ConversionPattern" value="%d %-5p [%c] %m%n" />
        </layout>
    </appender>
    <root>
        <level value="INFO" />
        <appender-ref ref="Console" />
        <appender-ref ref="File" />
    </root>
</log4j:configuration>

And here is how you can pass it on the server startup:
java -jar -Dlog4j.configuration=file:///home/apifest/log4j.xml apifest/target/apifest-0.1.2-SNAPSHOT-jar-with-dependencies.jar

3.2 Create a mappings configuration file

A mappings configuration file describes the different API resources/endpoints exposed as public ones, i.e. it defines your public API.
Let's see how a mapping configuration file look like:
<mappings version=[version]>
  <backend host=[backend_host] port=[backend_port]/>
  <endpoints>
      <endpoint external=[external_path] internal=[internal_path] method=[HTTP_METHOD] authType=[auth_type] scope=[scope_name] varName=[var_name] varExpression=[var_expression] backendHost=[backendHost] backendPort=[backendPort]>
      </endpoint>
  </endpoints>
  ...
</mappings>

The following tables describes each tag/attribute, its description, whether it's required or not and gives an example value.

tag (attribute) name description required example
mappings version The version of the public API this mappings configuration file describes. true <mappings version="v1">
backend Defines where the backend application is running, requests will be translated to that backend. Note,that all requests described in the mappings file will be pointed to that backend unless a specific backend per endpoint is set. true <backendhost="127.0.0.1" port="8181">
host The backend host true host="balancer.apifest.com"
port The backend port true port="80"
endpoint Defines an API resource that will be exposed.
external The URI path at which the resource will be available in the public API. true external="/v1/countries"
internal The URI path at which the resourceis available in the backend application. true internal="/common/countries"
method The HTTP method used to access the resource. true method="GET"
authType The type of the access token usedto access that resource - user OR client-app (user corresponds to passwordaccess token type and client-app - client_credentials access token type defined in OAuth 2.0 specification); if no authType is defined, then no access token is required to access the resource false authType="client-app"
authType="user"
scope Defines the allowed scope(s) of access tokens that can access that resource (see more details in chapter 3.10); if several scopes are allowed - use space as a delimiter; if no scope is defined, then any scope is allowed false scope="basic"
scope="basic extended"
varName Defines a space-separated list of variable(s) name(s) in the external/internalpath. false varName="countryCode"
varExpression Defines a space-separated list of regular expressions (Java format) that defines the variable(s) in the external/internal path. Note, the order of the regular expressions(s) should correspond to the order used in the varNamevalue. false varExpression="[A-Z]{3}"
backendHost Specific backend host for that endpoint will be used instead of the one defined in <backend...> tag. Note, that it will be used if both endpoint specific backendHost and backendPort are defined. false, the host and port defined in <backend> will be used backendHost="127.0.0.1"
backendPort Specific backend port for that endpoint will be used instead of the one defined in <backend...> tag. Note, that it will be used if both endpoint specific backendHost and backendPort are defined. false, the host and port defined in <backend> will be used backendPort="8181"
Here is a sample mappings configuration file:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<mappings version="v0.1">
    <backend port="5000" host="127.0.0.1"/>
    <endpoints>
      <endpoint external="/v0.1/countries" internal="/common/countries" method="GET" scope="basic" authType="client-app"/>
      <endpoint external="/v0.1/countries/{countryCode}" internal="/common/countries/{countryCode}" method="GET" scope="basic" authType="client-app" varExpression="[A-Z]{3}" varName="countryCode"/>
    </endpoints>
</mappings>

3.3 Mappings configuration validation

Once a mappings configuration file is created, it could be validated against the XML schema.
You can execute the following command (from the home directory of the apifest code) in order to validate the mappings configuration file you have created.
mvn validate -Pmapping-validation -Dmappings.file=[mapping_file]
where mapping_file is the path to the mappings configuration file.
If the mappings configuration file is not valid, an error message will be displayed and the build will fail, otherwise - the build will be successful and the following message will appear in the log:
INFO [com.apifest.MappingConfigValidator] mapping file [mapping_file] is valid

3.4 Create a custom action

For some requests a specific request transformation might be needed before the request hits you backend application. For instance, if you want to expose the following resource in your public API - GET /me that corresponds to GET /users/{userId} in your backend API, you will need a logic that somehow (we will see how that could be done later) extracts {userId}. That logic should be implemented as a custom Action class. An action in the ApiFest context means all such request transformations made before a request hits your backend application.
Each custom Action class should extend com.apifest.api.BasicAction class and implement its execute method. Let's see an example.
Here is a mappings configuration that describes the translation from the public call GET /v1.0/me to the internal one - GET /users/{userId}.
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<mappings version="v1.0">
  <backend port="5000" host="127.0.0.1"/>
  <endpoints>
    <endpoint external="/v0.1/me" internal="/users/{userId}" method="GET" scope="basic" authType="user">
      <action class="com.apifest.example.ReplaceUserIdAction"/>
    </endpoint>
  </endpoints>
</mappings>

As you can see the action class is a fully-qualified class defined to the endpoint. The class will be invoked while the request is translated from the public API to the backend application.

Here is the code of ReplaceUserIdAction class.
public class ReplaceUserIdAction extends BasicAction {

    protected static final String USER_ID = "{userId}";

    @Override
    public HttpRequest execute(HttpRequest req, String internalURI, HttpResponse tokenValidationResponse) {
        String newURI = internalURI.replace(USER_ID, BasicAction.getUserId(tokenValidationResponse));
        req.setUri(newURI);
        return req;
    }
}

The class (along with all other custom actions and filters) should be packaged in a jar and the path to the jar should be set in the custom.jar property in the apifest.properties file.

3.5 Create a custom filter

For some requests a response transformation might be needed. For instance, if your backend API returns the following response:
{"userId":"123456","email":"user@apifest.com","balance":"120"} to the request GET /users/123456 but for some reasons you don't want to return the user's balance, then you can use a filter. Of course, you can implement whatever response transformation you need.
Let's see how such a filter class might look like:
public class RemoveBalanceFilter extends BasicFilter {

    @Override
    public HttpResponse execute(HttpResponse response) {
        JsonParser parser = new JsonParser();
        JsonObject json = parser.parse(response.getContent().toString(CharsetUtil.UTF_8)).getAsJsonObject();
        json.remove("balance");
        byte[] newContent = json.toString().getBytes(CharsetUtil.UTF_8);
        response.setContent(ChannelBuffers.copiedBuffer(newContent));
        HttpHeaders.setContentLength(response, newContent.length);
        return response;
    }
}

Each custom Filter class should extend com.apifest.api.BasicFilter class and implement its execute method.
Here is the corresponding mapping configuration for that example:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<mappings version="v1.0">
  <backend port="5000" host="127.0.0.1"/>
  <endpoints>
    <endpoint external="/v0.1/me" internal="/users/{userId}" method="GET" scope="basic" authType="user">
      <action class="com.apifest.example.ReplaceUserIdAction"/>
      <filter class="com.apifest.example.RemoveBalanceFilter"/>
    </endpoint>
  </endpoints>
</mappings>

The class (along with all other custom filters and actions) should be packaged in a jar and the path to the jar should be set in the custom.jar property in the apifest.properties file.

3.6 Customized errors

If, for any reasons, you need to customize the responses from your backend API (let's say for different API versions), you might configure that in the mappings configuration file.
Let's see an example. For instance, your backend API returns the following response:
HTTP 404 {"error":"User with id 654321 not found"}
to the request GET /users/654321 but you want to return a unified HTTP 404 response error message, for instance: {"error":"resource not found"} for all HTTP 404 responses. Then you can configure that by using custom errors in the mappings configuration file:
<mappings>
    ...
    <errors>
        <error status="404" message='{"error":"resource not found"}'/>
     </errors>
</mappings>

This customization will be applied to the corresponding API version and when a HTTP 404 response is returned by the internal API. If you need to customize the error responses that are not related to mapped resources, i.e. the user tries to hit a resource that is not available, then you can use the customized global errors - see chapter 3.8.

3.7 Use Apifest Doclet to create mappings configuration files

You can use the ApiFest Doclet to generate the mappings configuration file automatically. However, you will need to put some Javadoc annotations in your code. For HTTP method, the ApiFest Doclet reads JAX-RS annotations - javax.ws.rs.GET, javax.ws.rs.POST, javax.ws.rs.PUT, javax.ws.rs.DELETE, javax.ws.rs.HEAD and javax.ws.rs.OPTIONS.

Here is a table that describes the ApiFest Javadoc annotations required to generate a mappings configuration file.

ApiFest Javadoc annotation description example
@apifest.external Corresponds to the endpoint external path (skip the API version here, it will be prepended when mappingconfiguration file is generated) @apifest.external/countries/{countryCode}
@apifest.internal Corresponds to the endpoint internal path. @apifest.internal/common/countries/{countryCode}
@apifest.scope Corresponds to OAuth 2.0 scope of the endpoint - a space-separatedlist of scopes. @apifest.scopebasic extended
@apifest.auth.type Corresponds to the endpoint authType - user or client-app. @apifest.auth.type client-app
@apifest.re.{var_name} Defines the regular expression used to map the corresponding var_name.
In case of several variables, add @apifest.re.{var_name} for each of them.
@apifest.re.countryCode[A-Z]{3}
@apifest.action Defines the action class (fully qualified name) that will be executed for an endpoint. com.apifest.example.ReplaceCustomerIdAction
@apifest.filter Defines the filter class (fully qualified name) that will be extecuted for an endpoint. com.apifest.example.RemoveBalanceFilter
@apifest.backend.host Corresponds to the endpoint specific backendHost. @apifest.backend.host127.0.0.1
@apifest.backend.port Corresponds to the endpoint specific backendPort. @apifest.backend.port8181


Here is an example of an annotated method:
/**
* @apifest.external /countries/{countryCode}
* @apifest.internal /countries/{countryCode}
* @apifest.re.countryCode [A-Z]{3}$
* @apifest.auth.type user
* @apifest.scope test_scope
*/
@GET
@Path("/{countryCode}")
public Response getByCode(@PathParam("countryCode") String countryCode);

In order to create a mappings configuration file using the ApiFest Doclet, you can add a maven profile in your pom.xml file. Here is an example:
<profile>
  <id>gen-mapping</id>
  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-javadoc-plugin</artifactId>
        <version>2.9.1</version>
        <dependencies>
          <dependency>
            <groupId>com.apifest</groupId>
            <artifactId>apifest-doclet</artifactId>
            <version>0.1.0</version>
          </dependency>
        </dependencies>
        <executions>
          <execution>
            <phase>validate</phase>
            <goals>
              <goal>javadoc</goal>
            </goals>
            <configuration>
              <doclet>com.apifest.doclet.Doclet</doclet>
              <docletArtifact>
                <groupId>com.apifest</groupId>
                <artifactId>apifest-doclet</artifactId>
                <version>0.1.0</version>
              </docletArtifact>
              <additionalJOptions>
                <additionalJOption>-J-Dmapping.version=v1.0</additionalJOption>
                <additionalJOption>-J-Dmapping.filename=mapping_v1_0.xml</additionalJOption>
                <additionalJOption>-J-Dbackend.host=127.0.0.1</additionalJOption>
                <additionalJOption>-J-Dbackend.port=8181</additionalJOption>
                <additionalJOption>-J-Dapplication.path=/common</additionalJOption>
              </additionalJOptions>
              <useStandardDocletOptions>false</useStandardDocletOptions>
            </configuration>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>
</profile>

Then you can use the following command in order to create the mappings configuration file:
mvn validate -Pgen-mapping

The following XML configuration will be produced:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<mappings version="v1.0">
    <backend port="8181" host="127.0.0.1"/>
    <endpoints>
        <endpoint external="/v1.0/countries/{countryCode}" internal="/common/countries/{countryCode}" method="GET" scope="test_scope" authType="user" varName="countryCode" varExpression="[A-Z]{3}$"/>
    </endpoints>
</mappings>

It will be stored in [project_home]/target/site/apidocs/mapping_v1_0.xml file.

The ApiFest Doclet expects the following environment variables:

name description required example
mapping.version The version of the API that will be exposed with the current mappings configuration file. true v1.0
mapping.filename The name of the mappings configuration file that will be created. false, the default value is output_mapping_[mapping.version].xml mappings_v1.0.xml
backend.host The host on which the internal API could be accessed. true 127.0.0.1
backend.port The port on which the internal API could be accessed. true 8181
application.path The application path that will be prepended to each endpoint path. false /commons
defaultActionClass The default action will be added to each endpoint if no specific endpoint action is set. false com.apifest.example.ReplaceUserIdAction
defaultFilterClass The default filter will be added to each endpoint if no specific endpoint filter is set. false com.apifest.example.RemoveBalanceFilter

If you do not use maven to build your project, you can use the ApiFest Doclet as a standard doclet to generate mappings configuration file. Here is how to run the ApiFest Doclet on the command line:
javadoc -docletpath %JAVA_HOME%/lib/tools.jar";[PATH_TO_apifest-api.jar];[PATH_TO_apifest-doclet.jar -classpath [all_jars_required] -doclet com.apifest.doclet.Doclet -J-Dmapping.version=[API_version] -J-Dmapping.filename=[output_mappings_file] -J-Dbackend.host=[backend_host] -J-Dbackend.port=[backend_port] -J-Dapplication.path=[application_path] -J-DdefaultActionClass=[default_action_class] -J-DdefaultFilterClass=[default_filter_class] [your_package]

3.8 Customized global errors

ApiFest enables you to customize the error response messages, when the errors are not related to a mappings configuration. These are the cases when a resource is not mapped at all, something goes wrong with the token validation or something goes wrong when the request is mapped and Internal Server Error is thrown. Then the following default responses will be returned:
Status Code: 404 Not Found
Content-Length: 21
Content-Type: application/json
Response Body: {"error":"Not found"}
and
Status Code: 401 Unauthorized
Content-Length: 33
Content-Type: application/json
Response Body: {"error":"access token required"}
and
Status Code: 500 Internal Server Error

You can change the error response message using the custom global errors. Note, that the responses are in JSON format and the customized global errors are expected to be in JSON format, too.

The description of these error responses are not related to an API version that's why they are described in a separate XML configuration file. Here is an example of a global errors configuration file:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<global-errors>
  <error status="401" message='{"error":"authorization required"}'/>
  <error status="404" message='{"error":"resource not found"}'/>
  <error status="500" message='{"error":"ops...something went wrong"}'/>
</global-errors>

The file path should be set as apifest.global-errors property in the apifest.properties file.
Then if you try to access a resource that is not available in the public API, you will receive the following response:
Status Code: 404 Not Found
Content-Length: 30
Content-Type: application/json
Response Body: {"error":"resource not found"}

Any changes in the file will be reloaded on ApiFest online configuration reload (see chapter 3.11).

3.9 Customized global errors validation

Once a mappings configuration file is created, it could be validated against the XML schema.
You can execute the following command (from the home directory of the apifest code) in order to validate the mappings configuration file you have created:
mvn validate -Pglobal-errors-validation -Dglobal-errors.file=[global-errors_file]
where global-errors_file is the path to the customized global errors configuration file.

If the global errors configuration file is not valid, an error message will be displayed and the build will fail, otherwise - the build will be successful and the following message will appear in the log:
INFO [com.apifest.GlobalErrorsConfigValidator] global errors file [global-errors_file] is valid

3.10 Scope definition

The scope in the mappings configuration file defines the OAuth 2.0 scope of the access tokens that are permitted to access a given endpoint. Let's say the following endpoint is defined in the mappings configuration file:
<endpoints>
    <endpoint external="/v0.1/me" internal="/users/{userId}" method="GET" scope="basic" authType="user">
      <action class="com.apifest.example.ReplaceUserIdAction"/>
      <filter class="com.apifest.example.RemoveBalanceFilter"/>
    </endpoint>
  </endpoints>

The scope defined is basic, that means that this endpoint could be accessed with access tokens issued with scope basic. If one tries to access the endpoint with an access token that has another scope - extended, for instance, then the following response will be returned:
Status Code: 401 Unauthorized
Content-Length: 40
Content-Type: application/json
Response Body: {"error":"access token scope not valid"}

The scope may be defined as a space-separated list of scopes that are allowed for the endpoint, for instance scope="basic extended". Then the permitted access tokens will have either basic or extended scope or both. As the ApiFest Mapping Server may reload its mappings configuration online, that enables online changes of endpoint scopes - just edit the scope in the mappings configuration file and then call GET /apifest-reload.
See more about the scope in chapter 3.10.

3.11 Online configuration reload

The mappings configuration and the customized global errors configuration could be updated online while the ApiFest Server is running. That helps you to expose a new endpoint online or to change the error response message online.
In order to reload these configurations, one needs to call GET /apifest-reload service. Note, that it's enough to call the service on one of the APiFest Mapping Server instances (if you are running several instances as a cluster) as these configurations are shared among all ApiFest Mapping Server instances.
In order to check the currently loaded mappings, you can use GET /apifest-mappings.
Here is an example response:
{
  "v1.0": {
    "mappings": {
      "com.apifest.MappingPattern@56eff2b4": {
        "backendPort": 5000,
        "backendHost": "127.0.0.1",
        "authType": "user",
        "scope": "test_scope",
        "method": "GET",
        "internalEndpoint": "/users/{userId}",
        "externalEndpoint": "/v1.0/me",
        "action": {
          "actionClassName": "com.apifest.example.ReplaceUserIdAction"
        }
      },
      "com.apifest.MappingPattern@e1a60a8b": {
        "backendPort": 5000,
        "backendHost": "127.0.0.1",
        "varExpression": "[A-Z]{3}",
        "varName": "countryCode",
        "authType": "client-app",
        "scope": "test_scope",
        "method": "GET",
        "internalEndpoint": "/countries/{countryCode}",
        "externalEndpoint": "/v1.0/countries/{countryCode}"
      }
    },
    "actions": {
      "com.apifest.example.ReplaceUserIdAction": "com.apifest.example.ReplaceUserIdAction"
    },
    "filters": {
    },
    "errors": {
      "404": "{\"error\":\"resource not found\"}"
    }
  }
}

In order to check the currently loaded customized global errors, you can use GET /apifest-global-errors.
Here is an example response:
{
  "401": "{\"error\":\"unauthorized\"}",
  "404": "{\"error\":\"resource not mapped\"}"
}

3.12 Managing API versions

Multiple versions support in ApiFest is easy to maintain. When you change the version of your API, you will need to change the mapping.version variable when you generate the mappings configuration file using the ApiFest Doclet (see chapter 3.7). You can put the new mappings configuration file in the mappings path where the old version configuration file is placed. After a call GET /apifest-reload, the both versions will be available.