a simple web based chat application built with Kotlin, Vaadin, Spring Boot and Apache Kafka

Intro

In this post I show how to combine some language / frameworks and libraries / tools to build a web-based scalable chat application. I chose the following combination of tools:

As I am bad in creating cool names for projects I just put together the first letters of the used tools and named this whole thing kovasbak. The complete source code and project is available on GitHub.

What it will look like

The following screenshot shows four browser windows with four users chatting:

Running the backend

The first thing that I have to do is to get Apache Kafka running. I downloaded the actual version (0.11.0.0) from the Apache Kafka website and unpacked the download in a local directory. According to the Kafka documentation I started first zookeeper and then one Kafka broker:

./bin/zookeeper-server-start.sh config/zookeeper.properties &
./bin/kafka-server-start.sh config/server.properties &

I am just using the default values, that gets Kafka runnning on port 9092.

Setting up the project

I am using Java 1.8.0_131 and IntelliJ IDEA, but the project is totally maven based, so you can use the IDE / editor of your choice. To create the project, I used the Spring Intializr integration in IntelliJ, but of course you can create the project by using the Spring Initializr website.

I just selected Kotlin as language, Java version 1.8, Spring Boot 1.5.4 and additionally selected web/vaadin and io/kafka.

After creating the project you end up with the following pom.xml, I only added the highlighted lines to be able to have server-push (more on that later):

<?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.sothawo</groupId>
  <artifactId>kovasbak</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <packaging>jar</packaging>

  <name>kovasbak</name>
  <description>a simple chat system built with Kotlin, Vaadin, spring Boot and Apache Kafka</description>

  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>1.5.4.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
  </parent>

  <properties>
    <kotlin.compiler.incremental>true</kotlin.compiler.incremental>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    <java.version>1.8</java.version>

    <kotlin.version>1.1.3</kotlin.version>
    <vaadin.version>8.0.6</vaadin.version>
  </properties>

  <dependencies>
    <dependency>
      <groupId>org.springframework.kafka</groupId>
      <artifactId>spring-kafka</artifactId>
    </dependency>
    <dependency>
      <groupId>com.vaadin</groupId>
      <artifactId>vaadin-spring-boot-starter</artifactId>
    </dependency>
    <dependency>
      <groupId>com.vaadin</groupId>
      <artifactId>vaadin-push</artifactId>
    </dependency>
    <dependency>
      <groupId>org.jetbrains.kotlin</groupId>
      <artifactId>kotlin-stdlib-jre8</artifactId>
      <version>${kotlin.version}</version>
    </dependency>
    <dependency>
      <groupId>org.jetbrains.kotlin</groupId>
      <artifactId>kotlin-reflect</artifactId>
      <version>${kotlin.version}</version>
    </dependency>

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
    </dependency>
  </dependencies>

  <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>com.vaadin</groupId>
        <artifactId>vaadin-bom</artifactId>
        <version>${vaadin.version}</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
    </dependencies>
  </dependencyManagement>

  <build>
    <sourceDirectory>${project.basedir}/src/main/kotlin</sourceDirectory>
    <testSourceDirectory>${project.basedir}/src/test/kotlin</testSourceDirectory>
    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
      </plugin>
      <plugin>
        <artifactId>kotlin-maven-plugin</artifactId>
        <groupId>org.jetbrains.kotlin</groupId>
        <version>${kotlin.version}</version>
        <configuration>
          <compilerPlugins>
            <plugin>spring</plugin>
          </compilerPlugins>
          <jvmTarget>1.8</jvmTarget>
        </configuration>
        <executions>
          <execution>
            <id>compile</id>
            <phase>compile</phase>
            <goals>
              <goal>compile</goal>
            </goals>
          </execution>
          <execution>
            <id>test-compile</id>
            <phase>test-compile</phase>
            <goals>
              <goal>test-compile</goal>
            </goals>
          </execution>
        </executions>
        <dependencies>
          <dependency>
            <groupId>org.jetbrains.kotlin</groupId>
            <artifactId>kotlin-maven-allopen</artifactId>
            <version>${kotlin.version}</version>
          </dependency>
        </dependencies>
      </plugin>
    </plugins>
  </build>


</project>

The code

In this post I will only show the relevant lines from the code, I will skip package and import statements, the full code is available at GitHub.

The application class

The application class created by the initializr just gets one additional line:

@SpringBootApplication
@EnableKafka
class KovasbakApplication

fun main(args: Array<String>) {
    SpringApplication.run(KovasbakApplication::class.java, *args)
}

The @EnableKafka annotation is used to tell Spring Boot to pull in the kafka related classes and libs.

The UI classes

ChatDisplay

The ChatDisplay is the Panel displaying the chat messages. I first used a TextArea, but had problems with programmatically scrolling to the bottom. So I created this small class that uses a Label to display the data:

class ChatDisplay : Panel() {
    val text: Label

    init {
        setSizeFull()
        text = Label().apply { contentMode = ContentMode.HTML }
        content = VerticalLayout().apply { addComponent(text) }
    }

    fun addMessage(user: String, message: String) {
        text.value = when {
            text.value.isNullOrEmpty() -> "<em>$user:</em> $message"
            else -> text.value + "<br/><em>$user:</em> $message"
        }
        scrollTop = Int.MAX_VALUE
    }
}

ChatUI

This is the main UI class:

@SpringUI
@PreserveOnRefresh
@Push
class ChatUI : UI(), KafkaConnectorListener {

    lateinit var user: String
    val chatDisplay = ChatDisplay()
    val userLabel = Label()

    @Autowired
    lateinit var kafkaConnector: KafkaConnector

    // skipping content here....

    companion object {
        val log: Logger = LoggerFactory.getLogger(ChatUI::class.java)
    }
}

It is marked as a Vaadin UI with @SpringUI, @PreserveOnRefresh keeps the session when the browser is reloaded, and @Push marks this for server-push when new messages arrive from Kafka. The class implements an interface KafkaConnectorListener which is described together with the KafkaConnector class.

The ChatUI has the following fields:

  • user: the name of the user that is chatting
  • chatDisplay: the display panel for the messages
  • userLabel: sits at the bottom left to show the name of the user
  • kafkaConnector: used for sending the own messages and to register for getting the messages from kafka

It further has a companion object containing the Logger. I now show the methods of the class:

override fun init(vaadinRequest: VaadinRequest?) {
    kafkaConnector.addListener(this)
    content = VerticalLayout().apply {
        setSizeFull()
        addComponents(chatDisplay, createInputs())
        setExpandRatio(chatDisplay, 1F)
    }
    askForUserName()
}

private fun createInputs(): Component {
    return HorizontalLayout().apply {
        setWidth(100F, Sizeable.Unit.PERCENTAGE)
        val messageField = TextField().apply { setWidth(100F, Sizeable.Unit.PERCENTAGE) }
        val button = Button("Send").apply {
            setClickShortcut(ShortcutAction.KeyCode.ENTER)
            addClickListener {
                kafkaConnector.send(user, messageField.value)
                messageField.apply { clear(); focus() }
            }
        }
        addComponents(userLabel, messageField, button)
        setComponentAlignment(userLabel, Alignment.MIDDLE_LEFT)
        setExpandRatio(messageField, 1F)
    }
}

This sets up the basic layout with the ChatDisplay and the other UI elements, registers the ChatUI with the KafkaConnector. The click handler for the send button is set up so that the user name and the content of the message TextField are sent to the KafkaConnector (see marked line).

After setting up the layout, the user is asked for her name with the following method:

private fun askForUserName() {
    addWindow(Window("your user:").apply {
        isModal = true
        isClosable = false
        isResizable = false
        content = VerticalLayout().apply {
            val nameField = TextField().apply { focus() }
            addComponent(nameField)
            addComponent(Button("OK").apply {
                setClickShortcut(ShortcutAction.KeyCode.ENTER)
                addClickListener {
                    user = nameField.value
                    if (!user.isNullOrEmpty()) {
                        close()
                        userLabel.value = user
                        log.info("user entered: $user")
                    }
                }
            })
        }
        center()
    })
}

This shows a modal window where the user’s name must be entered.

There is a method that is called when the UI is disposed:

override fun detach() {
    kafkaConnector.removeListener(this)
    super.detach()
    log.info("session ended for user $user")
}

The code used to send the actual message to the kafka connector was already shown, the last thing in this class is the code that is called from the KafkaConnector when new messages arrive:

override fun chatMessage(user: String, message: String) {
    access { chatDisplay.addMessage(user, message) }
}

The received data is added to the chatDisplay, but this is wrapped as a Runnable in the UI.access() method for two reasons:

  1. the code is asynchronously from a different thread and must be wrapped to be run on the UI thread.
  2. Executing the code in access() in combination with the @Push annotation on the class results in a server push to the client which is necessary so that the new messages are immediately shown.

The Kafka connector class

All communication with Kafka is wrapped in a Spring Component (thus being a singleton) which just has the following code:

interface KafkaConnectorListener {
    fun chatMessage(user: String, message: String)
}

@Component
class KafkaConnector {

    val listeners = mutableListOf<KafkaConnectorListener>()

    fun addListener(listener: KafkaConnectorListener) {
        listeners += listener
    }

    fun removeListener(listener: KafkaConnectorListener) {
        listeners -= listener
    }

    @Autowired
    lateinit var kafka: KafkaTemplate<String, String>

    fun send(user: String, message: String) {
        log.info("$user sending message \"$message\"")
        kafka.send("kovasbak-chat", user, message)
    }

    @KafkaListener(topics = arrayOf("kovasbak-chat"))
    fun receive(consumerRecord: ConsumerRecord<String?, String?>) {
        val key: String = consumerRecord.key() ?: "???"
        val value: String = consumerRecord.value() ?: "???"
        log.info("got kafka record with key \"$key\" and value \"$value\"")
        listeners.forEach { listener -> listener.chatMessage(key, value) }
    }

    companion object {
        val log: Logger = LoggerFactory.getLogger(KafkaConnector::class.java)
    }
}

First I defined the KafkaConnectorListener interface which the ChatUI class implements so they can be registered for new messages.

The KafkaConnector has a list of listeners and the methods to add and remove listeners. Nothing special here.

For sending a new message to kafka, the send method uses the injected KafkaTemplate (which comes from the spring-kafka library) to send the data to kafka by using the username as key and the message text as payload. The topic name that is used is kovasbak-chat.

By marking the receive method with @KafkaListener the method is called every time when a message in kafka arrives from any client. The data is parsed for the username and message body and the it is sent to all the registered clients. And finally there is a companion object with a Logger.

The configuration

spring.kafka.consumer.group-id=${random.uuid}
spring.kafka.consumer.auto-offset-reset=latest
spring.kafka.bootstrap-servers=localhost:9092

I use a random kafka consumer-group id so that each instance of my webapp gets all messages, I am not interested in old messages and define the host and port of the kafka broker.

Fire it up

You can either run the program from within the IDE or go to the command line and:

mvn package
java -jar target/kovasbak-0.0.1-SNAPSHOT.jar

you can then as well start a second instance on a different port like and access the servers on both localhost:8080 and localhost:8081

java -jar target/kovasbak-0.0.1-SNAPSHOT.jar --server.port=8081

Conclusion

To sum it up: with just a handful of code lines we have a scalable web-based chat-service which uses a scalable backend for message processing.

Using Server-Push in a Vaadin app

Just a short post to show how easy it is with Vaadin 7 to use server push in an application.

When doing some tasks in a background thread in a Vaadin application, there probably will come the time when some data needs to be shown in the UI. But as these data changes come from background processing, there is no direct possibility to show them in the UI, as UI changes normally are only reflected after a communication roundtrip from client to server to client which happens as a reaction to some user interaction. So even if you update your UI components in a UI.access() call – which is needed for correct access from the worker thread – these changes will not show in the UI until the next click. This is not what the user wants.

As a solution for this, Vaadin offers server push, the following shows how to implement it. I am using a Vaadin-Spring-Boot project to create the basis setup. The UI just contains a label which will be constantly updated from a background thread with the current time.

The pom is modified to include the following dependency:

<dependency>
    <groupId>com.vaadin</groupId>
    <artifactId>vaadin-push</artifactId>
    <version>7.4.5</version>
</dependency>

Then the UI class is in addition to the @SpringUI annotation decorated with @Push.
The consequence of this is, that the framework after every call to access() will use a server push mechanism to update the UI in the browser.

/**
 * Copyright (c) 2015 sothawo
 *
 * http://www.sothawo.com
 */
package demo;

import com.vaadin.annotations.Push;
import com.vaadin.annotations.Theme;
import com.vaadin.server.VaadinRequest;
import com.vaadin.spring.annotation.SpringUI;
import com.vaadin.ui.Label;
import com.vaadin.ui.UI;
import com.vaadin.ui.VerticalLayout;

import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.ForkJoinPool;

/**
 * UI class.
 *
 * @author P.J. Meisch (pj.meisch@sothawo.com).
 */
@Theme("valo")
@SpringUI
@Push
public class DemoUI extends UI {
    @Override
    protected void init(VaadinRequest vaadinRequest) {
        VerticalLayout layout = new VerticalLayout();
        layout.setMargin(true);
        layout.setSpacing(true);

        final Label labelTime = new Label("???");
        layout.addComponent(labelTime);

        // now in a background thread we constantly update the time
        final DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("HH:mm:ss");
        ForkJoinPool.commonPool().submit(() -> {
            boolean keepGoing = true;
            while (keepGoing) {
                access(() -> labelTime.setValue(LocalTime.now().format(dateTimeFormatter)));
                try {
                    Thread.sleep(600);
                } catch (InterruptedException e) {
                    keepGoing = false;
                }
            }
        });

        setContent(layout);
    }
}

And this makes the browser happily display the always current time:

push-time

 

Custom theme for a Vaadin – Spring-boot application

Recently I wrote an application based on Vaadin Spring-boot, and when I needed to modify the theme of the application I needed to do some research as how to achieve that. I think that at the time the combination of Vaadin and Spring-boot is still pretty new so that information still must be sought. So in this post I describe the necessary steps.

I use the following versions of different tools:

  • Oracle JDK 1.8.0_45
  • vaadin 7.4.5
  • vaadin-spring-start 1.0.0.beta3
  • spring-boot 1.2.3.RELEASE
  • spring 4.1.6.RELEASE

My IDE is IntelliJ IDEA 14.1.3

Creating the basic application

I create the application project (named vsbt for vaadin spring boot theme) by using the Spring Initializr from within IDEA, but it can be done via the Website https://start.spring.io as well:

vsbt01

For this demo I set up a project with the following properties that has only Vaadin as a dependency:

vsbt02

vsbt03

After finishing the setup I have an IDEA maven project with the basic application class, but still with no UI:

vsbt04

So to have something visible, I add a MainUI class which uses the valo theme and which just inserts a button in the UI. The button is nonfunctional, as this demo is only concerning with theming the UI and not with functionality:

/**
 * Copyright (c) 2015 sothawo
 *
 * http://www.sothawo.com
 */
package org.sothawo.vsbt;

import com.vaadin.annotations.Theme;
import com.vaadin.server.VaadinRequest;
import com.vaadin.spring.annotation.SpringUI;
import com.vaadin.ui.Button;
import com.vaadin.ui.UI;
import com.vaadin.ui.VerticalLayout;

/**
 * @author P.J. Meisch (pj.meisch@sothawo.com).
 */
@Theme("valo")
@SpringUI
public class MainUI extends UI {
    @Override
    protected void init(VaadinRequest vaadinRequest) {
        VerticalLayout layout = new VerticalLayout();
        layout.setSizeFull();
        layout.setMargin(true);
        layout.setSpacing(true);

        layout.addComponent(new Button("This is a button"));

        setContent(layout);
    }
}

Compiling and running the application gives the following result in the browser:

vsbt05

Changing the theme

Now for changing the theme. I want to create a theme named colored where I change some colors. The first step ist to put the theme files (sass files) in the src/main/webapp/VAADIN/themes/colored folder:

vsbt06

 

The styles.scss file:

@import "addons.scss";
@import "colored.scss";

The colored.scss file:

@import "../valo/valo.scss";

@mixin colored {
  @include valo;

  .v-app {
    background-color: red;
  }

  .v-button {
    background-image: none;
    background-color: yellow;
  }
}

@include colored;

I don’t get into the details of sass files here. In the MainUI class the annotation for the theme must be changed to @Theme("colored").

When I now start the program after building the program with the following command:

mvn clean package
cd target
java -jar vsbt-0.0.1-SNAPSHOT.jar

then the browser shows no theme:

vsbt07

The reason for this is that the files that are located under the src/main/webapp directory are not considered when the jar is packaged, this is only done when building a war file. So the first step that needs to be done is to specify this directory as a resource directory in the maven pom.xml:

<build>
  <resources>
    <resource>
      <directory>src/main/resources</directory>
    </resource>
    <resource>
      <directory>src/main/webapp</directory>
    </resource>
  </resources>
  ...
</build>

One might be tempted to move the VAADIN/themes/colored folder to src/main/resources, but there is still a problem to be solved. When building and running the application, the logging output shows an error message like:

un 02, 2015 9:21:14 PM com.vaadin.server.VaadinServlet persistCacheEntry
WARNUNG: Error persisting scss cache /private/var/folders/xw/1zt9sly53_76h29g7067_y700000gp/T/tomcat-docbase.6378846904000133878.8080/VAADIN/themes/colored/styles.scss.cache
java.io.FileNotFoundException: /private/var/folders/xw/1zt9sly53_76h29g7067_y700000gp/T/tomcat-docbase.6378846904000133878.8080/VAADIN/themes/colored/styles.scss.cache (No such file or directory)
        at java.io.FileOutputStream.open0(Native Method)

This happens because the sass compiler – at least on my Mac – has problems with persisting the compiled css file. To remove that error it is necessary to add the sass compiler to the compile step in the maven pom.xml (and this is the reason to leave the files in the src/main/webapp directory, it’s there that the compiler searches for them):

<dependencies>
        <dependency>
            <groupId>com.vaadin</groupId>
            <artifactId>vaadin-themes</artifactId>
        </dependency>
</dependencies>
...
<plugins>
    <plugin>
        <groupId>com.vaadin</groupId>
        <artifactId>vaadin-maven-plugin</artifactId>
        <executions>
            <execution>
                <goals>
                    <goal>update-theme</goal>
                    <goal>compile-theme</goal>
                    <!--
                    <goal>clean</goal>
                    <goal>resources</goal>
                    <goal>update-widgetset</goal>
                    <goal>compile</goal>
                    -->
                </goals>
            </execution>
        </executions>
    </plugin>
</plugins>

When now compiling the project, the addons.scss and the colored.css files are created in the theme directory and packaged in the application, and after packaging and running, the browser shows the following application with no more logged errors:

vsbt08

I hope this post can help if somebody needs to theme a vaadin spring-boot application and has the same problems finding out where to put the files and how to compile them.