Monday, May 9, 2016

SPARK - rest framework for java


Don't be surprised : ApacheSpark and SparkJava - it's a 2 different technologies !
In this post a'm talking about SparkJava - simple rest framework for Java :  http://sparkjava.com/

Related posts :
Spring Boot - simple example
SpringBoot with SpringData and H2 database

SpringBoot is a very good framework, but it takes as dependencies almost full stack of all Spring libraries. And if you are not planing to use them in your project - you will start thinking about more "compact" rest frameworks. SparkJava is one of them.

1. Goal 

As a lazy developer I want to create a rest application with ability to add new entities in very simple way : by just extending "base" class :
GenericController<Item> itemController=new GenericController<Item>("/item", Item.class, itemService);



2. Application structure

Standard "mave-based" application structure:


3.Maven project(pom.xml) file

Just spark,slfj,gson and junit dependencies:

<?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0"         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.demien</groupId>
    <artifactId>sparktest</artifactId>
    <version>1.0-SNAPSHOT</version>

    <dependencies>
    <dependency>
        <groupId>com.sparkjava</groupId>
        <artifactId>spark-core</artifactId>
        <version>2.0.0</version>
    </dependency>
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-simple</artifactId>
        <version>1.7.7</version>
    </dependency>
        <dependency>
            <groupId>com.google.code.gson</groupId>
            <artifactId>gson</artifactId>
            <version>2.2.4</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
        </dependency>

    </dependencies>


</project>


4. Domain objects

For my POJO data objects i created interface  IPersistable
public interface IPersistable {
    Long getId();
    void setId(Long id);

}

For having ability to operate ID field(by getId method) in controllers and test classes. So, all my domain objects are implementing this interface :

public class Item implements IPersistable {

    private Long id;
    private String name;
    private Long parentId;

public class Param implements IPersistable {

    private Long id;
    private String name;
    private String dataType;
    private Item item;

5. Controller

As I mentioned before, I'm too lazy, so I want to move common operations like "add", "get", "update", "delete" into one class :

package com.demien.sparktest.controller;

import com.demien.sparktest.util.JsonUtil;
import com.demien.sparktest.domain.IPersistable;
import com.demien.sparktest.service.GenericService;
import spark.Request;
import spark.Response;
import spark.Spark;

public class GenericController<T extends IPersistable> {
    private GenericService<T> service;
    private Class<T> cl;

    public GenericController(String basePath, Class<T> cl, GenericService<T> service) {
        this.cl=cl;
        this.service=service;
        Spark.get(basePath,this::getAll, JsonUtil::toJson);
        Spark.get(basePath+"/:id",this::getById, JsonUtil::toJson);
        Spark.get(basePath+"/test",this::test, JsonUtil::toJson);
        Spark.post(basePath,this::add, JsonUtil::toJson);
        Spark.put(basePath,this::update, JsonUtil::toJson);
        Spark.delete(basePath,this::delete, JsonUtil::toJson);
    }

    public Object test(Request request, Response response) {
        return "Hello world!";
    }

    public Object getAll(Request request, Response response) {
        return service.getAll();
    }

    public Object getById(Request request, Response response) {
        String id = request.params(":id");
        return service.getById(Long.parseLong(id));
    }

    public T restoreObjectFromRequest(Request request) {
        return (T)JsonUtil.toObject(request.body(),cl);
    }

    public Object add(Request request, Response response) {
        return service.add(restoreObjectFromRequest(request));
    }

    public Object update(Request request, Response response) {
        return service.update(restoreObjectFromRequest(request));
    }

    public Object delete(Request request, Response response) {
        service.delete(restoreObjectFromRequest(request));
        return "";
     }




}

So, now, for my entities(item and param) I have just to extend this class, without creation of controllers  :
GenericController<Item> itemController=new GenericController<Item>(ITEM_PATH, Item.class, itemService);
GenericController<Param> paramController=new GenericController<Param>(PARAM_PATH, Param.class, paramService);


6. "Dummy" service. 

It's just a simple demo project, so I decided not to use Hibernate, and created simple class for storing objects in a HashMap :

package com.demien.sparktest.service;

import com.demien.sparktest.domain.IPersistable;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class GenericService<T extends IPersistable> {
    private Long maxId=0L;

    private Map<Long, T> storage=new HashMap<Long, T>();

    public void clearStorage() {
        storage.clear();
    }

    public T getById(Long id) {
        return storage.get(id);
    }

    public T add(T element) {
        Long id=element.getId();
        if (id==null) {
            maxId++;
            element.setId(maxId);
        } else {
            if (maxId.longValue()<id.longValue()) {
                maxId=id+1;
            }
        }
        return update(element);
    }

    public List<T> getAll() {
        List<T> result=new ArrayList<T>();
        for (T element:storage.values()) {
            result.add(element);
        }
        return result;
    }

    public T update(T element) {
        storage.put(element.getId(), element);
        return storage.get(element.getId());
    }

    public void delete(T element) {
        storage.remove(element.getId());
    }

}

7. Main application file. 

Spark is running just like regular java application. In main() procedure i have to "start" my controllers :
package com.demien.sparktest;

import com.demien.sparktest.controller.GenericController;
import com.demien.sparktest.domain.Param;
import com.demien.sparktest.service.GenericService;
import spark.Spark;
import com.demien.sparktest.domain.Item;

public class App {

    public final static int SPARK_PORT=8080;
    public final static String APP_PATH="http://localhost:"+SPARK_PORT;

    public final static GenericService<Item> itemService=new GenericService<>();
    public final static String ITEM_PATH="/item";

    public final static GenericService<Param> paramService=new GenericService<>();
    public final static String PARAM_PATH="/param";

    public static void main(String[] args) {
        Spark.setPort(8080);
        GenericController<Item> itemController=new GenericController<Item>(ITEM_PATH, Item.class, itemService);
        GenericController<Param> paramController=new GenericController<Param>(PARAM_PATH, Param.class, paramService);
    }

}

8. Utils

Also I had to create few simple utils :


8.1. JsonUtil  - just for conversion json<=>object

package com.demien.sparktest.util;

import com.google.gson.Gson;

public class JsonUtil {
    public static String toJson(Object object) {
        return new Gson().toJson(object);
    }

    public static Object toObject(String json, Class<?> cl) {
        return new Gson().fromJson(json, cl);
    }
}


8.2. RestTestUtil  - for testing : sending requests 

package com.demien.sparktest.util;

import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;

public class RestTestUtil {

    public static class RequestResult {

        public final String body;
        public final int status;

        private RequestResult(int status, String body) {
            this.body = body;
            this.status = status;
        }
    }

    public static RequestResult sendRequest(String method, String path, String urlParameters) throws IOException {

        URL url = new URL(path);
        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
        conn.setDoOutput(true);
        conn.setInstanceFollowRedirects(false);
        conn.setRequestMethod(method);

        if (urlParameters!=null) {
            byte[] postData = urlParameters.getBytes(StandardCharsets.UTF_8);
            int postDataLength = postData.length;

            conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
            conn.setRequestProperty("charset", "utf-8");
            conn.setRequestProperty("Content-Length", Integer.toString(postDataLength));
            conn.setUseCaches(false);
            conn.getOutputStream().write(postData);
        }

        Reader in = new BufferedReader(new InputStreamReader(conn.getInputStream(), "UTF-8"));
        StringBuilder sb = new StringBuilder();
        for (int c; (c = in.read()) >= 0; )
            sb.append((char) c);
        String responseBody = sb.toString();
        int responseCode=conn.getResponseCode();
        return new RequestResult(responseCode, responseBody);

    }

    public static RequestResult sendRequest(String method, String path) throws IOException {
        return sendRequest(method, path, null);
    }


}

8.3 object populator - for testing : to "fill" test object with random generated data. 

package com.demien.sparktest.util;

import com.demien.sparktest.domain.IPersistable;
import com.demien.sparktest.domain.Item;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

public class ObjectPopulator {

    interface RandomGenerator {
        Object getRandomValue();
    }

    enum DataType {
        Integer(() -> {
            return new Integer((int) (Math.random() * 1000));
        }),
        Long(() -> {
            return new Long((long) (Math.random() * 1000));
        }),
        Date(()-> {
            return new Date(new Date().getTime() - (int) (Math.random() * 1000 * 60 * 60 * 24 * 100));
        }),
        String(() -> {
            StringBuffer result = new StringBuffer();
            String[] letters = new String[]{"A", "B", "C", "D", "E", "F", "G"};
            int length = (int) (Math.random() * 15) + 5;
            for (int i = 0; i < length; i++) {
                int pos = (int) (Math.random() * letters.length);
                result.append(letters[pos]);
            }
            return result.toString();
        }
        );

        private RandomGenerator generator;

        DataType(RandomGenerator generator) {
            this.generator = generator;
        }

        Object getRandomValue() {
            return generator.getRandomValue();
        }
    }

    public static Object populate(IPersistable instance) throws IllegalAccessException {
        List<Field> fields = getAllFields(instance);
        for (Field eachField : fields) {
            eachField.setAccessible(true);
            String typeName=eachField.getType().getSimpleName();
            if (eachField.getType().getTypeName().startsWith("com.demien")) {
                Object obj=null;
                try {
                     obj=eachField.getType().newInstance();
                } catch (InstantiationException e) {
                    e.printStackTrace();
                }
                obj=populate((IPersistable) obj);
                eachField.set(instance, obj);
            } else {
                DataType dataType = DataType.valueOf(typeName);
                eachField.set(instance, dataType.getRandomValue());
            }
        }
        return instance;
    }


    private static List<Field> getAllFields(Object instance) {
        Field[] fields = instance.getClass().getDeclaredFields();
        List<Field> result = new ArrayList<Field>();
        for (int i = 0; i < fields.length; i++) {
            if (!java.lang.reflect.Modifier.isFinal(fields[i].getModifiers())
                    && !java.lang.reflect.Modifier.isStatic(fields[i]
                    .getModifiers())) {
                result.add(fields[i]);
            }
        }
        return result;
    }

 
}

9. Generic integration test class

For base operations i created generic controller test class - other controllers will just extend it :
package com.demien.sparktest;

import com.demien.sparktest.domain.IPersistable;
import com.demien.sparktest.service.GenericService;
import com.demien.sparktest.util.JsonUtil;
import com.demien.sparktest.util.ObjectPopulator;
import com.demien.sparktest.util.RestTestUtil;
import org.junit.*;
import spark.Spark;

import java.util.List;

public abstract class GenericControllerIT<T extends IPersistable> {
    private String fullPath;
    private Class<T> cl;
    private GenericService<T> service;

    public GenericControllerIT(String basePath, Class<T> cl, GenericService<T> service){
        this.fullPath= App.APP_PATH+basePath;
        this.cl=cl;
        this.service=service;
    }

    @BeforeClass    public static void init() {
        App.main(null);
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    @AfterClass    public static void tearDown() {
        Spark.stop();
    }

    @Before    public void initTest() {
        service.clearStorage();
    }

    T getTestObject() throws Exception {
        T testObject=cl.newInstance();
        ObjectPopulator.populate(testObject);
        return testObject;
    }

    @Test    public void addTest() throws Exception {
        T testObject=getTestObject();
        RestTestUtil.RequestResult res= RestTestUtil.sendRequest("POST", fullPath, JsonUtil.toJson(testObject));
        Assert.assertEquals(200, res.status);
        Assert.assertEquals(service.getById(testObject.getId()), testObject);
    }

    @Test    public void getTest() throws Exception {
        T testObject=getTestObject();
        service.add(testObject);
        RestTestUtil.RequestResult res= RestTestUtil.sendRequest("GET", fullPath +"/"+testObject.getId());
        Assert.assertEquals(200, res.status);
        T receivedObject=(T)JsonUtil.toObject(res.body, cl);
        Assert.assertEquals(testObject, receivedObject);
    }

    @Test    public void getAllTest() throws Exception {
        T testObject1=getTestObject();
        service.add(testObject1);
        T testObject2=getTestObject();
        service.add(testObject2);
        RestTestUtil.RequestResult res= RestTestUtil.sendRequest("GET", fullPath);
        Assert.assertEquals(200, res.status);
        List<T> receivedObjectList=(List<T>)JsonUtil.toObject(res.body, java.util.List.class);
        Assert.assertTrue(receivedObjectList.size()==2);
    }


    @Test    public void updateTest() throws Exception {
        T testObject=getTestObject();
        service.add(testObject);
        T updatedObject=getTestObject();
        updatedObject.setId(testObject.getId());

        RestTestUtil.RequestResult res= RestTestUtil.sendRequest("PUT", fullPath, JsonUtil.toJson(updatedObject));
        Assert.assertEquals(200, res.status);

        T updatedObjectFromService=service.getById(testObject.getId());
        Assert.assertEquals(updatedObject, updatedObjectFromService);
    }

    @Test    public void deleteTest() throws Exception {
        T testObject=getTestObject();
        service.add(testObject);

        RestTestUtil.RequestResult res= RestTestUtil.sendRequest("DELETE", fullPath, JsonUtil.toJson(testObject));
        Assert.assertEquals(200, res.status);

        T deletedObject=service.getById(testObject.getId());
        Assert.assertNull(deletedObject);
    }





}


10. Integration test classes for ItemController and ParamController. 

Now we can just extend GenericControlerIT class and all tests for base operation will be inherited :

package com.demien.sparktest;

import com.demien.sparktest.domain.Item;

public class ItemControllerIT extends GenericControllerIT<Item> {
    public ItemControllerIT() {
        super(App.ITEM_PATH, Item.class, App.itemService);
    }
}


package com.demien.sparktest;

import com.demien.sparktest.domain.Param;

public class ParamControllerIT extends GenericControllerIT<Param> {
    public ParamControllerIT() {
        super(App.PARAM_PATH, Param.class, App.paramService);
    }
}

11. The end

Complete source code can be downloaded from here.