Java: avoiding ‘instanceOf’ operator

An interesting way to avoid ‘instanceOf’ operator (some developers hate it) is to use below mapper:

 

class Mapper {

        Map<Class, Function<Object, String>> mappers = Maps.newHashMap();

        Mapper() {
            mappers.put(Operator.class, o -> ((Operator) o).getSign());
            mappers.put(Boolean.class, Object::toString);
            mappers.put(Integer.class, Object::toString);
            mappers.put(Double.class, Object::toString);
            mappers.put(Long.class, Object::toString);
            mappers.put(String.class, s -> String.format("\"%s\"", s));
            mappers.put(List.class, l -> {
                StringBuilder stringBuilder = new StringBuilder("[");
                Iterator i = ((List) l).iterator();
                while (i.hasNext()) {
                    Object o = i.next();
                    stringBuilder.append(Mapper.this.map(o));
                    if (i.hasNext()) {
                        stringBuilder.append(",");
                    }
                }
                stringBuilder.append("]");
                return stringBuilder.toString();
            });
        }

        public String map(Object value) {
            for (Map.Entry<Class, Function<Object, String>> entry : mappers.entrySet()) {
                Class key = entry.getKey();
                if (key.isInterface() && asList(value.getClass().getInterfaces()).contains(key)
                    || Objects.equals(key, value.getClass())) {
                    return entry.getValue().apply(value);
                }
            }
            throw new UnsupportedOperationException();
        }
    }
Advertisements

AWS: S3 operations

To list all objects use:

private Try<List<S3ObjectSummary>> listAllFiles(String bucket, String key) {
        ListObjectsRequest listObjectsRequest = new ListObjectsRequest().withBucketName(bucket).withPrefix(key);
        return Try.of(() -> {
            final List<ObjectListing> objectListingList = new LinkedList<>();

            ObjectListing objectListing = amazonS3.listObjects(listObjectsRequest);
            objectListingList.add(objectListing);

            while (objectListing != null && objectListing.isTruncated()) {
                objectListing = amazonS3.listNextBatchOfObjects(objectListing);
                objectListingList.add(objectListing);
            }

            return objectListingList.stream()
                .flatMap(objectListing1 -> objectListing1.getObjectSummaries().stream())
                .collect(Collectors.toList());
        });
    }

Read file:

public Try<String> getBytes(String bucket, String key) {
        log.info("Reading data from: " + bucket + "/" + key);
        try (InputStream s3is = amazonS3.getObject(bucket, key).getObjectContent();
             ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
            byte[] buf = new byte[8192];
            int bytesRead;
            while ((bytesRead = s3is.read(buf)) != -1) {
                outputStream.write(buf, 0, bytesRead);
            }
            return Try.success(outputStream.toString());
        } catch (IOException e) {
            return Try.failure(e);
        }
    }

Java: Request Dispatcher

In one of the previous posts i mentioned about AWS there was ‘Request dispatcher’ presented very briefly.

Here I would like to spend a few minutes to write about it in more details.

This is all we need to create the ‘request dispatcher’:

java-request-dispatcher-project-view

Let’s begin with simple POJO that will provide us the Token and Url-Path – the ‘DispatchRequest.java’:

public class DispatchRequest {

    private String path;
    private String token;

    public DispatchRequest() {
    }

    public DispatchRequest(String path, String token) {
        this.path = path;
        this.token = token;
    }

    public String getPath() {
        return path;
    }

    public void setPath(String path) {
        this.path = path;
    }

    public String getToken() {
        return token;
    }

    public void setToken(String token) {
        this.token = token;
    }
}<span id="mce_SELREST_start" style="overflow:hidden;line-height:0;"></span>

We also might need some execution context as we needed in AWS Lambda implementation. This may or may not be neccessary depanding on the project.

In this implementation the execution context holds all system environments passed from main Lambda function.

public class DispatchContext {

    private final String property1;
    private final String property2;
    private final String property3;
    private final String property4;
    private final String property5;
    private final String property6;
    private final String property7;
    private final String property8;
    private final String property9;
}

Along with the context it is convinient to have a builder to ease the creation of the DispatchContext object.

Finally, we have the main service that dispatches the execution upon the provided TOKEN and PATH:

import com.fasterxml.jackson.databind.ObjectMapper;
import io.vavr.API;
import io.vavr.control.Try;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import java.io.IOException;
import java.io.OutputStream;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

public class RequestDispatcher {

    private static final Logger log = LogManager.getLogger(RequestDispatcher.class);

    private String apiVersion;

    public Try dispatch(DispatchContext dispatchContext, DispatchRequest dispatchRequest, OutputStream os) {
        final String token = dispatchRequest.getToken();
        final String bucket = dispatchContext.getBucketWithInputData();
        final ObjectMapper om = new ObjectMapper();

        return
            API.Match(dispatchRequest.getPath()).of(
                //@formatter:off
                API.Case(API.$("/" + apiVersion + "/healthcheck"),               () -> runHealthCheck(dispatchContext, os, om)),
                API.Case(API.$("/" + apiVersion + "/configuration"),             () -> runConfiguration(dispatchContext, os)),
                API.Case(API.$("/" + apiVersion + "/resource-1"),                () -> authorize(token).andThenTry(() -> runResource1(bucket,  dispatchContext(), os))),
                API.Case(API.$("/" + apiVersion + "/resource-2"),                () -> authorize(token).andThenTry(() -> runResource2(bucket,  dispatchContext(), os))),
                API.Case(API.$("/" + apiVersion + "/resource-2/sub-resource-1"), () -> authorize(token).andThenTry(() -> runResource21(bucket, dispatchContext(), os))),
                API.Case(API.$("/" + apiVersion + "/resource-2/sub-resource-2"), () -> authorize(token).andThenTry(() -> runResource22(bucket, dispatchContext(), os))),
                API.Case(API.$(), () -> Try.failure(new UnsupportedEndpointException(dispatchRequest.getPath())))
                //@formatter:on
            );
    }
}

Of course, this could be implemented in a lot of different ways. Moreover this is a simplified version due to the fact that AWS API-Gateway takes the responsibility for generating the HTTP responses i.e. 200 CODE. If the dispatcher had to deal with the generation of the full HTTP request it would have gotten more complicated.

Anyways, I like the implementation as it is clear and neat, in my opinion.

 

AWS: API-Gateway – Lambda proxy integration

Sometimes it might be more convinient to use ‘Lambda proxy integration’ to handle all requests.

The code below requires:

It means that we pass the whole request to lambda (with no posibility to enhance it) and we let lambda build the full response.

Let’s have a look how such handling might look like in Java code:

import static io.vavr.API.$;
import static io.vavr.API.Case;

import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent;
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent;
import com.fasterxml.jackson.core.JsonProcessingException;
import io.vavr.API;
import io.vavr.collection.Seq;
import io.vavr.control.Try;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;

import java.util.HashMap;
import java.util.Map;

@SuppressWarnings("unused")
public class Lambda {

    private static final Logger log = LogManager.getLogger(Lambda.class);

    @SuppressWarnings("unused")
    public APIGatewayProxyResponseEvent dispatchRequests(APIGatewayProxyRequestEvent requestEvent, Context context) throws JsonProcessingException {
        final APIGatewayProxyResponseEvent responseEvent = new APIGatewayProxyResponseEvent();
        responseEvent.setHeaders(defaultHeaders());

        final Validation validationResult = validateRequest(requestEvent, API_VERSION);
        if (validationResult.isInvalid()) {
            return setStatusCodeAndBody(responseEvent, 400, validationResult.getError().toJavaList());
        }

        final ApiRequestValidator.ApiRequest apiRequest = validationResult.get();
        final Try authorizationResult = authorize(apiRequest.getAuthorization());
        if (authorizationResult.isFailure()) {
            return setStatusCodeAndBody(responseEvent, 401, authorizationResult.getCause());
        }

        // dispatch request
        API.Match(apiRequest.getPath()).of(
            Case($("/" + API_VERSION + "/resource"),              resourceHandler()),
            Case($("/" + API_VERSION + "/resource/sub-resource"), subResourceHandler()),
            Case($(), o -> {
                throw new UnsupportedEndpointException(o);
            })
        )
            .onSuccess(endpointResult -> setStatusCodeAndBody(responseEvent, 200, endpointResult))
            .onFailure(throwable -> setStatusCodeAndBody(responseEvent, 500, throwable.getLocalizedMessage()));

        return responseEvent;
    }

    private Map defaultHeaders() {
        Map result = new HashMap();
        result.put("Content-Type", "application/json");
        result.put("Access-Control-Allow-Origin", "*");
        return result;
    }

    private APIGatewayProxyResponseEvent setStatusCodeAndBody(APIGatewayProxyResponseEvent responseEvent, int statusCode, Object body) {
        responseEvent.setStatusCode(statusCode);
        setBody(responseEvent, body);
        return responseEvent;
    }

    private Validation validateRequest(APIGatewayProxyRequestEvent event, String apiPrefix) {
        return new ApiRequestValidator(apiPrefix).validateRequest(event.getHeaders(), event.getPath(), event.getHttpMethod());
    }

}

Code above introduces a few lines where:

  • status codes are set i.e. 200, 401, 500
  • ‘Content-Type’ or other flags are set in response’s header

Therefore, lambda takes full responsibility for handling the original requests that arrive at API-Gateway, than map the url path to specific controller to handle the logic, and at the end build a valid response.

In fact, there are not many things that API-Gateway can do….its role is simplified just to passing the requests and responses…

It can be easily noticed by looking at the API-Gataway’s proxy integration panel:

aws-api-gateway-lambda-proxy-integration-panel

The ‘Integration’ parts are disabled or reduced with its functionality, while these parts are crucial when it comes to handling the requests and responses. Since we are using ‘lambda proxy integration’ we delegate the responsibily of handling the requests and response directly to lambda.

Below is the view of ‘Integration Request’ panel:

aws-api-gateway-lambda-proxy-integration-request

To set up the ‘lambda integration proxy’ read AWS documentation:

 

AWS: API-Gateway: dispatching requests with one Lambda

Sometimes it is more convinient to use one lambda that handles many endpoints from API-Gateway.

We can achive this by:

  • setting the mapping in GET method in

aws-api-gateway-handle-many-endpoints-with-one-lambda

  • next, we need to handle the incomming JSON object that is defined above and dispatch the request on the ‘path’ variable
public class DispatchRequest {

    private String path;
    private String token;

    public DispatchRequest(String path, String token) {
        this.path = path;
        this.token = token;
    }

}
  • and a piece of code that does the dispatch:
import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestStreamHandler;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.vavr.API;
import io.vavr.control.Try;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import java.io.InputStream;
import java.io.OutputStream;

@SuppressWarnings("unused")
public class Lambda implements RequestStreamHandler {

    private static final Logger log = LogManager.getLogger(Lambda.class);
    private static final String apiVersion = "api-v1";

    @Override
    public void handleRequest(InputStream is, OutputStream os, Context context) {
        log.info("Triggered lambda");

        final ObjectMapper om = new ObjectMapper();
        final DispatchRequest apiRequest = om.readValue(is, DispatchRequest.class);

        final String path = apiRequest.getPath();
        final String token = apiRequest.getToken();

        API.Match(path).of(
            //@formatter:off
            API.Case(API.$("/" + apiVersion + "/some-resource"),               () -> runSomeCode() ),
            API.Case(API.$("/" + apiVersion + "/other-resource/sub-resource"), () -> authorize(token).andThenTry(() -> runOtherCode())),
            API.Case(API.$(), () -> Try.failure(new UnsupportedEndpointException(path)))
            //@formatter:on
        );

        log.info("Lambda finished.");
    }

}

 

AWS: API-Gateway – convert to binary

After creating a resource and a GET method, fill in the ‘Integration Response’ part for response 200.

aws-api-gateway-convert-to-binary

Now, all we need to to is make our lambda to return the data in ‘encodede’ way:

 
public class AmazonS3Wrapper {

    private static final Logger log = LogManager.getLogger(AmazonS3Wrapper.class);

    private AmazonS3 amazonS3;

    public AmazonS3Wrapper(AmazonS3 amazonS3) {
        this.amazonS3 = amazonS3;
    }

    public Try<Void> getBytes(String bucket, String key, OutputStream os) {
        log.info("Loading data from: " + bucket + "/" + key);
        try (InputStream s3is = amazonS3.getObject(bucket, key).getObjectContent();
             OutputStream os64 = Base64.getEncoder().wrap(os)) {
            byte[] buf = new byte[8192];
            int bytesRead;
            while ((bytesRead = s3is.read(buf)) != -1) {
                os64.write(buf, 0, bytesRead);
            }
        } catch (IOException e) {
            return Try.failure(e);
        }
        return Try.success(null);
    }
}

And run the code in main handler method:

import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestStreamHandler;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import java.io.InputStream;
import java.io.OutputStream;

@SuppressWarnings("unused")
public class Lambda implements RequestStreamHandler {

    private static final Logger log = LogManager.getLogger(Lambda.class);

    @Override
    public void handleRequest(InputStream is, OutputStream os, Context context) {
        log.info("Triggered lambda");

        final AmazonS3Wrapper amazonS3Wrapper = new AmazonS3Wrapper(amazonS3);
        amazonS3Wrapper.getBytes(bucket, key, os);

        log.info("Lambda finished.");
    }

}

Test smells: exceptions

A test written by “Senior Java Developer”.

Bravo, Mr Senior!! 🙂

@Test
  public void StaticInitBlock_NonDefaultProfile_LocalDataRepositoryWithoutIssueDir_ConfigurationErrorIsThrown() {
    //given
    nonDefaultProfile();
    System.setProperty("RunConfig_dataRepository", "Local");
    //when
    try {
      staticInitBlock();
    } catch (Throwable e) {
    //then
      Assert.assertTrue(e instanceof ConfigurationError);
    }
  }