Integration with UIKernel

Purpose

In this guide we will create simple CRUD web-application using both UIKernel and DataKernel

Introduction

UIKernel is a comprehensive React.js UI library for building forms, editable grids and reports with drilldowns and filters, based on simple unified record model with client-side and server-side validations and data bindings.

DataKernel Framework includes UIKernel module that greatly simplifies integration with UIKernel.io.

At a high level, DataKernel + UIKernel application architecture looks like following:



Both UIKernel module and UIKernel JS Library use the same conventions in HTTP requests and responses for performing CRUD operations. So, in back-end, we have a little of work:

  • write a POJO that extends AbstractRecord
  • write GridModel for this POJO

What you will need:

  • JDK 1.7 or higher
  • Maven 3.0
  • DataKernel (installation) version 2.0.9
  • Node
  • Bower, Gulp (npm i -g bower gulp)

What modules will be used:

  • UIKernel
  • HTTP
  • Boot

To proceed with this guide you have 2 options:

1. Working Example

To run the complete example, enter next commands:

$ git clone https://github.com/softindex/datakernel-examples
$ cd datakernel-examples/tutorials/uikernel-integration
$ npm i
$ gulp build
$ gulp run

Then open you favourite browser, go to “http://localhost:8080” and try to:

  • create new record
  • delete record
  • update data in cells

2. Step-by-step guide

In this guide we will mainly focus on back-end java components of application, and just copy and paste front-end part. If you want to learn more about front-end part, go to uikernel.io

Firstly, create a folder for application and build appropriate project structure:

uikernel-integration
└── pom.xml
└── gulpfile.js
└── ... additional config files for js
└── src
    └── main
        └── webapp
            └── ... js files
        └── resources
            └── static
                └── ... css, html, bundled js
        └── java
            └── io
                └── datakernel
                    └── examples
                        └── Gender.java
                        └── Person.java
                        └── PersonGridModel.java
                        └── UIKernelWebAppModule.java
                        └── UIKernelWebAppLauncher.java

As mentioned above, you should copy and paste js components to your project from this source, namely:

  • content of “resources” directory
  • content of “webapp” directory
  • bower.json
  • package.json
  • .bowerrc


Then, let’s configure pom.xml. It will use boot, uikernel and logger as dependencies and also exec plugin to simplify running of app:

<?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>io.datakernel</groupId>
    <artifactId>uikernel-integration</artifactId>
    <version>1.0</version>
    <packaging>jar</packaging>

    <properties>
        <datakernel.version>2.5.10-SNAPSHOT</datakernel.version>
        <main.class>io.datakernel.examples.UIKernelWebAppLauncher</main.class>
    </properties>

    <dependencies>
        <dependency>
            <groupId>io.datakernel</groupId>
            <artifactId>datakernel-boot</artifactId>
            <version>${datakernel.version}</version>
        </dependency>
        <dependency>
            <groupId>io.datakernel</groupId>
            <artifactId>datakernel-uikernel</artifactId>
            <version>${datakernel.version}</version>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.1.3</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.3</version>
                <configuration>
                    <source>1.7</source>
                    <target>1.7</target>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.codehaus.mojo</groupId>
                <artifactId>exec-maven-plugin</artifactId>
                <version>1.4.0</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>exec</goal>
                        </goals>
                    </execution>
                </executions>
                <configuration>
                    <executable>java</executable>
                    <arguments>
                        <argument>-classpath</argument>
                        <classpath/>
                        <argument>${main.class}</argument>
                    </arguments>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

Next, let’s configure gulp to enable building both js and java component. Let’s define the following lifecycle commands in gulp:

  • clean
  • test
  • build
  • pack
  • run

So, gulpfile.js should look like following:

'use strict';

var gulp = require('gulp');
var sync = require('gulp-sync')(gulp).sync;
var shell = require('gulp-shell');

var jsTasks = require('./src/main/webapp/gulp/javascript');

// Javascript tasks
gulp.task('js:clean', jsTasks.createBundle);
gulp.task('js:bundle', ['js:clean'], jsTasks.createBundle);

// Java tasks
gulp.task('java:clean', shell.task(['mvn clean']));
gulp.task('java:test', shell.task(['mvn test']));
gulp.task('java:compile', shell.task(['mvn compile']));
gulp.task('java:package', shell.task(['mvn package']));
gulp.task('java:run', shell.task(['mvn exec:exec']));

// General tasks
gulp.task('clean', ['js:clean', 'java:clean']);
gulp.task('test', ['java:test']);
gulp.task('build', ['js:bundle', 'java:compile']);
gulp.task('pack', sync(['js:bundle', 'java:package']));
gulp.task('run', ['java:run']);

gulp.task('default', ['build']);


Having this done, let’s write guice module and launcher for this app, without implementing model.

Guice module:

class UIKernelWebAppModule extends AbstractModule {
	private static final int DEFAULT_PORT = 8080;
	private static final String DEFAULT_PATH_TO_RESOURCES = "static/";

	@Override
	protected void configure() {
		bind(ExecutorService.class).toInstance(newCachedThreadPool());
	}

	@Provides
	@Singleton
	AsyncHttpServer server(Eventloop eventloop, ExecutorService executor) {
		// middleware used to map requests to appropriate asyncServlets
		StaticServlet staticServlet = StaticServletForResources.create(eventloop, executor, DEFAULT_PATH_TO_RESOURCES);
    MiddlewareServlet dispatcher = new MiddlewareServlet().withFallback(staticServlet) // serves request if no other servlet matches

		// configuring server
    return AsyncHttpServer.create(eventloop, dispatcher).withListenPort(port)
	}

  @Provides
	@Singleton
	Eventloop eventloop() {
		return Eventloop.create();
	}
}

Launcher:

public class UIKernelWebAppLauncher extends Launcher {

	public UIKernelWebAppLauncher() {
		super(Stage.PRODUCTION,
				ServiceGraphModule.defaultInstance(),
				PropertiesConfigModule.ofFile("configs.properties"),
				new UIKernelWebAppModule());
	}

	@Override
	protected void run() throws Exception {
		awaitShutdown();
	}

	public static void main(String[] args) throws Exception {
		Launcher.main(UIKernelWebAppLauncher.class, args);
	}
}


Now, let’s try to launch this app. To do this, go to project root directory and enter the following commands:

npm i
gulp build
gulp run


Then, open your favourite browser and go to “http://localhost:8080”. You should see web page like the one below:


After launching basic version of this app, let’s implement model. It will consist of single entity class Person:

public class Person extends AbstractRecord<Integer> {
	private String name;
	private String phone;
	private Integer age;
	private Gender gender;

	public Person() {
	}

	public Person(int id, String name, String phone, Integer age, Gender gender) {
		this.id = id;
		this.name = name;
		this.phone = phone;
		this.age = age;
		this.gender = gender;
	}

  /* getters, setters and toString() */
}


We should also implement GridModel interface which defines basic CRUD operations with convenient API.

public interface GridModel<K, R extends AbstractRecord<K>> {
	void create(R record, ResultCallback<CreateResponse<K>> callback);

	void read(K id, ReadSettings<K> settings, ResultCallback<R> callback);

	void read(ReadSettings<K> settings, ResultCallback<ReadResponse<K, R>> callback);

	void update(List<R> changes, ResultCallback<UpdateResponse<K, R>> callback);

	void delete(K id, ResultCallback<DeleteResponse> callback);

	Class<K> getIdType();

	Class<R> getRecordType();
}


In this guide GridModel for Person will store all the records in HashMap for simplicity. But you can also use another storage such as database or something else. You just need to forward CRUD operations to that storage using GridModel interface.

So let’s initialize our storage and fill some data:

class PersonGridModel implements GridModel<Integer, Person> {

  /* other fields */

	private final static Map<Integer, Person> storage = initStorage();

	private static Map<Integer, Person> initStorage() {
	  Map<Integer, Person> storage = new HashMap<>();
	  storage.put(1, new Person(1, "Humphrey", "555-0186", 77, FEMALE));
	  storage.put(2, new Person(2, "Thornton", "555-0108", 73, MALE));
	  storage.put(3, new Person(3, "Addie", "555-0105", 33, MALE));
	  storage.put(4, new Person(4, "Kelsey", "555-0189", 44, FEMALE));
	  storage.put(5, new Person(5, "Sonya", "555-0153", 33, FEMALE));
	  storage.put(6, new Person(6, "Adams", "555-0175", 88, MALE));
	  storage.put(7, new Person(7, "Rodriguez", "555-0133", 55, MALE));
	  storage.put(8, new Person(8, "Acosta", "555-0100", 22, MALE));
	  storage.put(9, new Person(9, "Murray", "555-0132", 99, MALE));
	  storage.put(10, new Person(10, "Francine", "555-0153", 63, FEMALE));
	  storage.put(11, new Person(11, "Angelica", "555-0125", 19, FEMALE));
	  storage.put(12, new Person(12, "Kaya", "555-0179", 34, FEMALE));
	  storage.put(13, new Person(13, "Roach", "555-0118", 55, MALE));
	  storage.put(14, new Person(14, "Evangeline", "555-0153", 18, FEMALE));
	  return storage;
	}

  /* method implementations */

}


Next, we have to implement CRUD methods.

The simplest are:

@Override
public void create(final Person person, final ResultCallback<CreateResponse<Integer>> callback) {
	person.setId(cursor);
	storage.put(cursor, person);
	callback.setResult(CreateResponse.of(cursor));
	cursor++;
}

@Override
public void read(final Integer id, final ReadSettings<Integer> readSettings, final ResultCallback<Person> callback) {
  callback.setResult(storage.get(id));
}

@Override
public void delete(final Integer id, final ResultCallback<DeleteResponse> callback) {
  storage.remove(id);
  callback.setResult(DeleteResponse.ok());
}


More complex operation is “bulk read”:

@Override
public void read(final ReadSettings<Integer> readSettings, ResultCallback<ReadResponse<Integer, Person>> callback) {
  Predicate<Person> predicate = new PersonMatcher(readSettings.getFilters());
  List<Person> people = newArrayList(filter(storage.values().iterator(), predicate));
  for (Map.Entry<String, ReadSettings.SortOrder> entry : readSettings.getSort().entrySet()) {
    Comparator<Person> comparator = comparators.get(entry.getKey());
    if (entry.getValue() == ReadSettings.SortOrder.DESCENDING) {
      comparator = Collections.reverseOrder(comparator);
    }
    Collections.sort(people, comparator);
  }
  int count = people.size();
  people = people.subList(readSettings.getOffset(), Math.min(people.size(), readSettings.getOffset() + readSettings.getLimit()));
  callback.setResult(ReadResponse.of(people, count, Collections.<Person>emptyList()));
}

One of method arguments is ReadSettings which contain information about:

  • fields
  • filters
  • limit and offset
  • ordering

We traverse our storage and determine for each person whether it satisfies query parameters or not. To do this let’s define a class PersonMatcher:

private static class PersonMatcher implements Predicate<Person> {
  private final Map<String, String> filters;

  PersonMatcher(Map<String, String> filters) {this.filters = filters;}

  @Override
  public boolean apply(Person person) {
    String searchParam = filters.get("search");
    if (searchParam != null && !searchParam.isEmpty()) {
      if (!person.getName().contains(searchParam)) {
        return false;
      }
    }

    String ageParam = filters.get("age");
    if (ageParam != null && !ageParam.isEmpty()) {
      int age = Integer.parseInt(ageParam);
      if (person.getAge() != age) {
        return false;
      }
    }

    String genderParam = filters.get("gender");
    if (genderParam != null && !genderParam.isEmpty() && (genderParam.equals("MALE") || genderParam.equals("FEMALE"))) {
      Gender gender = Gender.valueOf(genderParam);
      if (person.getGender() != gender) {
        return false;
      }
    }

    return true;
  }
}


Let’s write update():

@Override
public void update(final List<Person> changes, final ResultCallback<UpdateResponse<Integer, Person>> callback) {
  List<Person> results = new ArrayList<>();
  for (Person change : changes) {
    Person person = storage.get(change.getId());
    checkNotNull(person);
    merge(person, change);
    results.add(person);
  }
  callback.setResult(UpdateResponse.of(results));
}

private void merge(Person person, Person change) {
  if (change.getName() != null) {
    person.setName(change.getName());
  }
  if (change.getPhone() != null) {
    person.setPhone(change.getPhone());
  }
  if (change.getAge() != null) {
    person.setAge(change.getAge());
  }
  if (change.getGender() != null) {
    person.setGender(change.getGender());
  }
}


We should also implement getIdType() and getRecordType():

@Override
public Class<Integer> getIdType() {
  return Integer.class;
}

@Override
public Class<Person> getRecordType() {
  return Person.class;
}


Finally, let’s include our PersonGridModel in Guice module:

public class UIKernelWebAppModule extends AbstractModule {
	private static final int DEFAULT_PORT = 8080;
	private static final String DEFAULT_PATH_TO_RESOURCES = "static/";

	@Override
	protected void configure() {
		bind(ExecutorService.class).toInstance(newCachedThreadPool());
		bind(PersonGridModel.class).in(Singleton.class);
		bind(Gson.class).in(Singleton.class);
	}

	@Provides
	@Singleton
	AsyncHttpServer server(Eventloop eventloop, ExecutorService executor, Gson gson, PersonGridModel model, Config config) {
		String pathToResources = config.get(ofString(), "resources", DEFAULT_PATH_TO_RESOURCES);
		int port = config.get(ofInteger(), "port", DEFAULT_PORT);

		// middleware used to map requests to appropriate asyncServlets
		AsyncServlet usersApiServlet = UiKernelServlets.apiServlet(model, gson);
		StaticServlet staticServlet = StaticServletForResources.create(eventloop, executor, pathToResources);

		MiddlewareServlet dispatcher = MiddlewareServlet.create()
				.withFallback(staticServlet)                 // serves request if no other servlet matches
				.with("/api/users", usersApiServlet);        // our rest crud servlet that would serve the grid


		// configuring server
		return AsyncHttpServer.create(eventloop, dispatcher).withListenPort(port);
	}

	@Provides
	@Singleton
	Eventloop eventloop() {
		return Eventloop.create();
	}
}


Congratulation! We’ve finished writing code for this app.


Launch application again using next commands:

npm i
gulp build
gulp run

Now, after going to “http://localhost:8080” in your browser you should see a grid table with support of:

  • creating new records
  • updating data in cells
  • deleting records