JavaFX Project Setup

Introduction

Using JavaFX platform, we can build applications for desktop, mobile or embessed systems. It has large set of components to design rich user interface.

JavaFX allows,

  • designing the user interface interactively using Scene Builder.
  • testing the user inteface using TestFX.
  • styling the user interface with the help of CSS.

JavaFX historically used to part of Java installation, however, now it is a standalone component which works on top of JDK.

I faced multiple challenges during setting up the JavaFX with Java 21 to build a simple application recently. I would like to share the setup I ended up with which may help you as well.

We will be going to use following tools,

Here we will going to use Maven which will take care of downloading required modules, however, you can use JavaFX SDK as well.

Project Setup

IntelliJ bundles JavaFX plugin in it.

Create New Project in IntelliJ.

Select JavaFX in side bar. Select Project SDK as Java 21.

For now, we do not need any additional dependencies, we will just click on Finish.

This will take time finish setting up project and indexing.

Once it’s ready we can simply run the Main class which is HelloApplication.java in this case.

It should run successfully and show a window with a button “Hello!”.

As you can see some gibberish text is shown. You may see following logs in Console.

This looked like some issue with the fonts. So I had to add following line in main method of HelloApplication.java.

scene.getRoot().setStyle("-fx-font-family: 'serif'");

After running it again it appeared like following,

Changes in POM

If you go to Project Structure > Modules you will find many OpenFX dependencies are added.

If we look into pom.xml file, we can see following dependecies are add for JavaFX.

...
<dependency>
    <groupId>org.openjfx</groupId>
    <artifactId>javafx-controls</artifactId>
    <version>11.0.2</version>
</dependency>
<dependency>
    <groupId>org.openjfx</groupId>
    <artifactId>javafx-fxml</artifactId>
    <version>11.0.2</version>
</dependency>
...

By default the selected version is 11.0.2 which we will going to change to recent version 22.

<dependency>
    <groupId>org.openjfx</groupId>
    <artifactId>javafx-controls</artifactId>
    <version>22</version>
</dependency>
<dependency>
    <groupId>org.openjfx</groupId>
    <artifactId>javafx-fxml</artifactId>
    <version>22</version>
</dependency>

However, the build started failing with module error.

I had no clue what could have went wrong, so I did clean up and try to build again but didn’t work.

Surprisingly, when I executed with command line using mvn it worked!

mvn clean compile
mvn javafx:run

Strangely, there is no error in module-info.java, from command line it worked well but failing when I run it from IntelliJ. I felt this issue is something specific to IntelliJ and not to JavaFX.

I found the similar problem is discussed on the forum.

I was using IntelliJ CE version shown in following image,

Moreover, importing the same project to IntelliJ Ultimate Edition worked miraculously without any modifications.

The javafx-maven-plugin is already on latest version, thus we are not changing anything here for now.

...
<plugin>
    <groupId>org.openjfx</groupId>
    <artifactId>javafx-maven-plugin</artifactId>
    <version>0.0.8</version>
    <executions>
        <execution>
            <!-- Default configuration for running with: mvn clean javafx:run -->
            <id>default-cli</id>
            <configuration>
                <mainClass>com.example.hellojavafx/com.example.hellojavafx.HelloApplication</mainClass>
                <launcher>app</launcher>
                <jlinkZipName>app</jlinkZipName>
                <jlinkImageName>app</jlinkImageName>
                <noManPages>true</noManPages>
                <stripDebug>true</stripDebug>
                <noHeaderFiles>true</noHeaderFiles>
            </configuration>
        </execution>
    </executions>
</plugin>
...

Initialisation Error

During build and trying some maven command, I ended up with following error,

Error occurred during initialization of boot layer
java.lang.module.FindException: Module com.example.hellojavafx not found

some articles were suggesting to validate Java and JavaFX versions but that wasn’t the issue. Strangely, deleting target directory and running the application worked as it is.

Also, while running through command for the first time you may get following error,

[ERROR] Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.8.1:compile (default-compile) on project hello-javafx: Fatal error compiling: error: invalid target release: 0 

I noticed that in maven-compiler-plugin the source and target version was 0 which I changed to 21 to fix this issue.

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>3.8.1</version>
    <configuration>
        <source>0</source>
        <target>0</target>
    </configuration>
</plugin>

Module Info

Apart from pom.xml, you will find one module-info.java shown below. Here we can see module dependency on javafx.controls and javafx.fxml is mentioned. Also, javafx.fxml needs to access controllers we will define in out package thus we are opening it.

module com.example.hellojavafx {
    requires javafx.controls;
    requires javafx.fxml;


    opens com.example.hellojavafx to javafx.fxml;
    exports com.example.hellojavafx;
}

Whenever you introduce new dependency which you want to be available in your package or you want to open your package to external dependency. Make sure to declare it in module-info.java file.

Executable JAR

To make a runnable .jar I used maven-shade-plugin.

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-shade-plugin</artifactId>
    <version>2.3</version>
    <executions>
        <execution>
            <phase>package</phase>
            <goals>
                <goal>shade</goal>
            </goals>
            <configuration>
                <transformers>
                    <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                        <mainClass>com.example.hellojavafx.HelloApplication</mainClass>
                    </transformer>
                </transformers>
            </configuration>
        </execution>
    </executions>
</plugin>

I simple ran mvn clean package to create the jar file. However when I ran the jar with following command.

java -jar target/hello-javafx-1.0-SNAPSHOT.jar

It gave me following error,

Error: JavaFX runtime components are missing, and are required to run this application

To solve this, I had to declare an entry point class which can point to my Application class.

package com.example.hellojavafx;

public class EntryPoint {

    public static void main(String[] args) {
        HelloApplication.main(args);
    }
}

This new class I had to use in plugin instead of HelloApplication.

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-shade-plugin</artifactId>
    <version>2.3</version>
    <executions>
        <execution>
            <phase>package</phase>
            <goals>
                <goal>shade</goal>
            </goals>
            <configuration>
                <transformers>
                    <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                        <mainClass>com.example.hellojavafx.EntryPoint</mainClass>
                    </transformer>
                </transformers>
            </configuration>
        </execution>
    </executions>
</plugin>

And this time it worked! (with warning)

➜  hello-javafx java -jar target/hello-javafx-1.0-SNAPSHOT.jar
Aug 10, 2024 10:35:43 PM com.sun.javafx.application.PlatformImpl startup
WARNING: Unsupported JavaFX configuration: classes were loaded from 'unnamed module @673ba40c'

The shade plugin adds everything to the classpath and since JavaFX relies on modules this setup breaks the execution. Thus, as a workaround we introduce another class which can be an entry point to the execution instead of JavaFX starter application.

Basically, if JavaFX application is loaded through classpath it actually breaks the encapsulation provided by modular approach. Here is the ticket for this warning.

Base64 Encoder

Base64 is popular system that transforms the binary data into limited set of 64 characters.

For example, text abc can be represented in binary as 011000010110001001100011, Base64 divides this binay data into group of 6 bits 011000 010110 001001 100011 and converts it to as per the encoding table.

Base64 Encoding Table has total 64 (2^6) characters.

  • 0 to 25 : A to Z
  • 26 to 51 : a to z
  • 52 to 61 : 0 to 9
  • 62 : Plus symbol ‘+’
  • 63 : Forward slash ‘/’

Additional symbo ‘=’ is used for padding in case of bits are not in multiple of 6.

According to encoding table, 011000 010110 001001 100011 which is 24 (011000) 22 (010110) 9 (001001) 35 (100011) converted to YWJj in Base64 format.

public class Encoder {

    private static final Encoder ENCODER = new Encoder();

    private static final int ENCODE_BASE = 6;

    private final char[] encodingTable = new char[64];

    private Encoder() {
        initEncodingTable();
    }

    public static Encoder getInstance() {
        return ENCODER;
    }

    public String encode(String input) {
        int[] binary = toBinary(input);
        StringBuilder encoded = new StringBuilder();

        for (int start = 0; start < binary.length; start += ENCODE_BASE) {
            int end = Math.min(start + ENCODE_BASE, binary.length - 1);

            int power = ENCODE_BASE - 1;
            int charIndex = 0;
            for (int j = start; j < end; j++) {
                charIndex += (binary[j] * Math.pow(2, power--));
            }

            encoded.append(encodingTable[charIndex]);
        }

        int pad = binary.length % ENCODE_BASE;
        encoded.append("=".repeat(pad));
        return encoded.toString();
    }

    private int[] toBinary(String input) {
        final int bits = 8;
        int[] binary = new int[input.length() * bits];

        try {
            byte[] bytes = input.getBytes(StandardCharsets.UTF_8);
            int index = 0;
            int max = (int) Math.pow(2, (bits - 1));

            for (byte b : bytes) {
                int val = b;
                for (int i = 0; i < bits; i++) {
                    binary[index++] = ((val & max) == 0 ? 0 : 1);
                    val <<= 1;
                }
            }
        } catch (RuntimeException e) {
            e.printStackTrace();
        }
        return binary;
    }

    private void initEncodingTable() {
        int index = 0;

        for (char i = 'A'; i <= 'Z'; i++)
            encodingTable[index++] = i;

        for (char i = 'a'; i <= 'z'; i++)
            encodingTable[index++] = i;

        for (char i = '0'; i <= '9'; i++)
            encodingTable[index++] = i;

        encodingTable[62] = '+';
        encodingTable[63] = '/';
    }
}

This program is implemented to make it more easier to understand. However, for optimal approach we can look into java.util.Base64 class which is bit complex to understand and considers much bigger character set and scenarios than the implemented one.

5 mistakes I did as a Software Developer

Undoubtedly, mistakes are part of everyone's life. The challenge is to learn from the mistakes and avoid repeating them in future. 

As a programmer, you will gradually improve in career by contributing to different projects, coding contests, pair programming etc. But the major aspect of improvement lies in learning from the mistakes.

Today I will talk about 5 mistakes that I did as Software Developer that I could have avoided.

1. Try to focus more on solution rather than problem

This mistake is not only applies to coding but in general to any problem of our lives. We are so desperately invested in the solution that we forget about the problem or we do not pay enough attention to the problem.

I learned that the more we focus on problem the better solution we can deliver, that will not just give good value to the customer but also a deep and clear insight into the solution as well.

2. Deliver things without enough Test Cases

I have underrated the value of unit testing and especially TDD for a long time. The confidence you get on your work comes from enough and solid test cases, is imppacable. 

The value you build is not just immediate for your confirmation or avoiding issues in your delivery but this is for all those developers writing the code which may impact your changes and can confidently make changes in the same code because of efforts you put in for writing required test cases.

3. Waiting for "good" project to contribute

There is nothing like good or bad project that I learned over course of time. It's a problem and solution cycle for every software developer which we keep on focusing for. If you are looking for a good project or good company, you may wait for longer. 

To learn and experiment anything, do not wait for project to come to you that's a reactive approach, go ahead and create something from the technology which interests you.

4. Shy away from learning from Seniors

You will find many mentors in your career, try to gain as much knowlege you can gain from them. You need to be proactively discuss solutions and understanding with them. This will give you a different perspective and opportunity to align your thought process with the experienced folks.

You would argue that you can learn from courses, youtube or chatgpt. Well you should and you woul learn from the internet but the conversations you will do with your mentors can not be replaced with internet for sure that I learned gradually. 

5. Hesitate to ask questions

I always stopped myself from asking the questions during discussions and meetings. Many times the question came to my mind was asked by someone else, and I regreted. Asking questions during presentations, pair programming, team meetings etc. rewards you with many things. Your team understands that you are interested to learn, you get required clarity, you gain confidence, the facilitator feels that you are engaged into the conversation so and so forth. 

However, on contrary if you don't ask, you mainly miss the easy opportunity to practice courage. When your inner  self is resisting and still you ask the question, see how it feels. Moreoever, you missed an opportunity of curiously seeking ansers from the learned ones which you may not easily get answer of, on your own.


There are tons of other mistakes, but these 5 I believe could have turned me in a different direction initially, and those who are going to be software developers in the future may learn from them and avoid them as much as possible for better growth and satisfaction. 

Some useful stream operations

Separate Odd and Even numbers (List<Integer> to Map<String, List<Integer>>)

Map<String, List<Integer>> oddEvenMap = numbers.stream()  
        .collect(Collectors.groupingBy(i -> (i % 2 != 0 ? "Odd" : "Even")));

Find distinct values from Map<String, List<Integer>

Set<Integer> set = map.values().stream()
					.flatMap(Collection::stream)
					.collect(Collectors.toSet());

Biggest Odd and Even from List (List<Integer> to Map<String, Integer>)

Map<String, Integer> bigOddEven = numbers.stream().collect(Collectors.toMap(i -> (i % 2 == 0 ? "Even" : "Odd"),  Function.identity(), (i1 , i2) -> i1 > i2 ? i1 : i2));

Use value as key and create new map from existing

Map<String, Integer> map = new HashMap<>();  
map.put("a", 1);  
map.put("b", 1);  
map.put("c", 2);  
  
Map<Integer, List<String>> ans = map.keySet().stream().collect(Collectors.groupingBy(map::get));  
System.out.println(ans);

Output

{1=[a, b], 2=[c]}

Find frequency of each number in list

List<Integer> numbers = new ArrayList<>();  
numbers.add(1);  
numbers.add(2);  
numbers.add(3);  
numbers.add(4);  
numbers.add(4);  
  
Map<Integer, Long> ans = numbers.stream()  
        .collect(Collectors.groupingBy(Function.identity(), Collectors.counting()));  
System.out.println(ans);

Output

{1=1, 2=1, 3=1, 4=2}

Remove from map based on condition on values

Map<String, Integer> map = new HashMap<>();  
map.put("a", 1);  
map.put("b", 1);  
map.put("c", 2);  
  
map.values().removeIf(i -> i == 1);  
System.out.println(map);

Output

{c=2}

Distinct and Sum of numbers

Stream<Integer> distinct = numbers.stream().distinct();
int s = numbers.stream().mapToInt(Integer::intValue).sum();

IntSummaryStatistics for Average, Sum, Min/Max

IntSummaryStatistics intSummaryStatistics = numbers.stream().mapToInt(Integer::intValue).summaryStatistics();
double average = intSummaryStatistics.getAverage();
long sum = intSummaryStatistics.getSum();
int max = intSummaryStatistics.getMax();
int min = intSummaryStatistics.getMin();
long count = intSummaryStatistics.getCount();

Build SVG in nodeJS

Purpose

I noticed tags shown in Github with the help of link. That made me curious to know more about it. There are certain ways to do it but the interesting one I found was dynamically building and returing the SVG image which can directly render in markdown file. I came to know about Shields which builds beautiful dynamic badges as per the need.

The Way

I wanted to create a basic setup which can render the tag directly in the markdown file. The first step was to create a svg content. We can do it in many ways, however to create dynamic tags I decided to go with nodeJS.

Building svg from scratch is quite tricky, I got to know about Roughjs, which made things even simpler. Initial idea was to dynamically build tag as per the input sent in query parameter. It was a bit struggle initially to get it working. However, I came up with following function to achieve that.

With the help of monspace fonts, we can exactly calculate the width and height required for the tag. I tried to keep it as dynamically as possible.

const { DOMImplementation, XMLSerializer } = require('xmldom');
const xmlSerializer = new XMLSerializer();
const document = new DOMImplementation().createDocument('http://www.w3.org/1999/xhtml', 'html', null);

const rough = require('roughjs');

//...

function createSVG(text, color) {
    console.log(text);
    if(!text || text.length == 0) {
        text = '?';
    }

    if(text.length > maxTagSize) {
        text = text.substr(0, maxTagSize);
    }

    text = text.trim().toUpperCase();

    var textLength = text.length;

    var width = (fontWidth * textLength) + fontWidth;
    var height = fontSize * 1.5;

    var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
    const rc = rough.svg(svg);
    
    var tagName = document.createElementNS("http://www.w3.org/2000/svg", "text");
    tagName.setAttribute("x", (fontWidth / 2));
    tagName.setAttribute("y", fontSize + 1);
    tagName.setAttribute('style', 'font-family:monospace;font-weight:bold;font-size:'+fontSize+'px;');
    tagName.textContent = text;

    var rectangle = rc.rectangle(0, 0, width , height, {roughness: 0, fill : color, fillStyle: 'solid', stroke: '#8BD8E2'});
    
    svg.setAttribute("width", width);
    svg.setAttribute("height", height);
    svg.appendChild(rectangle);
    svg.appendChild(tagName);
    return svg;
}

Later I deployed this on Google Cloud Function to make it accessible for markdown by supplying function URL in HTML image tag source as shown below.

    <img src="https://tags.akashthakare.com/generateTag?title=java" />

Next

This was a very interesting idea which I can publish and used in my own GitHub profile or even in readme files of repositories.

I would like to make it more interesting going forward. I am thinking to introduce icons or even images in it. This way it will give flexibility to caller of the function to build different styles of tag with different content.

This can be implemented without using RoughJS. I will see if I can do it without using it.

If you are interested, you can check the full code here.