Search

Dark theme | Light theme

March 18, 2025

SDKMAN! Listing And Upgrading Outdated SDKs And Tools

SDKMAN! is as powerful tool to install and manage software development kits (SDKs) and tools, like Java, Groovy, Gradle, Maven, Spring Boot and Quarkus. If you want to see if a new version of a SDK or tool is available, you can use the sdk upgrade command. This command will list all outdated SDKs and tools. The installed version and the latest version are shown for each SDK and tool. To see if a single SDK or tool is outdated, you can use the name of the SDK or tool as argument to the sdk upgrade command.

In the following example you see the output of a sdk upgrade command for the Spring Boot tool. By answering Y (default answer) to the question Use prescribed default version(s)? you can upgrade the Spring Boot tool to the latest version:

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ sdk upgrade springboot
 
Available defaults:
springboot (local: 3.1.5, 3.2.4; default: 3.4.3)
 
Use prescribed default version(s)? (Y/n):
 
Downloading: springboot 3.4.3
 
In progress...
 
####################################################################### 100.0%
 
Installing: springboot 3.4.3
Done installing!
 
 
Setting springboot 3.4.3 as default.
$

You can also run sdk upgrade without an argument and the output shows all SDKs and tools that are outdated. You can upgrade all outdated SDKs and tools by answering Y to the question Use prescribed default version(s)?. In the following example you see the output of a sdk upgrade command without an argument:

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
$ sdk upgrade
 
Available defaults:
groovy (local: 4.0.25, 4.0.24, 4.0.23, 3.0.22, 4.0.20; default: 4.0.26)
java (local: 20-tem, 21-tem, 11.0.22-tem, 23-tem, 11.0.25-tem, 21-zulu, 19.0.2-tem, 19.0.1-tem, 17.0.10-tem, 17.0.13-tem, 11.0.8.hs-adpt, 23-graalce, 8.0.432-tem, 21.0.1-tem, 21.0.3-tem, 21.0.2-tem, 21.0.5-tem, 21.0.4-tem; default: 21.0.6-tem)
 
Use prescribed default version(s)? (Y/n):
 
Downloading: groovy 4.0.26
 
In progress...
 
####################################################################### 100.0%
 
Installing: groovy 4.0.26
Done installing!
 
 
Setting groovy 4.0.26 as default.
 
Downloading: java 21.0.6-tem
 
In progress...
 
####################################################################### 100.0%
 
Repackaging Java 21.0.6-tem...
Restored java version to 21.0.4-tem (default)
 
Using java version 11.0.25-tem in this shell.
 
Done repackaging...
Cleaning up residual files...
 
Installing: java 21.0.6-tem
Done installing!
 
 
Setting java 21.0.6-tem as default.
$

Written with SKDMAN! 5.19.0.

March 14, 2025

Nushell Niceties: Getting The HTTP Response Status

Nushell has a built-in command to invoke HTTP requests: http. You don’t need an external tool like curl or httpie to make HTTP requests. There a lot of options to use with the http command. One of them is the --full or shorter -f option to return a table with extra details of the HTTP request and response. The request and response headers, the body and status are returned in the table. You can easily get information from the table with all the default selection options for a table structure.

In the following example you see the table structure that is returned for a http get request with and without the --full option:

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
> http get https://www.mrhaki.com/nushell.txt # Without --full you get only the response body.
Nushell rocks!
> http get --full https://www.mrhaki.com/nushell.txt # Use --full to get the full response.
╭─────────┬────────────────────────────────────────────────────────────────────────────────────────────╮
│         │ ╭──────────┬─────────────────────────────────────────────────────────────────────────────╮ │
│ headers │ │ request  │ [list 0 items]                                                              │ │
│         │ │          │ ╭────┬───────────────────────────┬────────────────────────────────────────╮ │ │
│         │ │ response │ │  # │           name            │                 value                  │ │ │
│         │ │          │ ├────┼───────────────────────────┼────────────────────────────────────────┤ │ │
│         │ │          │ │  0 │ age                       │ 51                                     │ │ │
│         │ │          │ │  1 │ cache-status              │ "Netlify Edge"; hit                    │ │ │
│         │ │          │ │  2 │ cache-control             │ public,max-age=0,must-revalidate       │ │ │
│         │ │          │ │  3 │ content-type              │ text/plain; charset=UTF-8              │ │ │
│         │ │          │ │  4 │ content-length            │ 14                                     │ │ │
│         │ │          │ │  5 │ x-nf-request-id           │ 01JP9NJNTBY60HF0TR8X2R3J0K             │ │ │
│         │ │          │ │  6 │ server                    │ Netlify                                │ │ │
│         │ │          │ │  7 │ etag                      │ "010320ca2351da56022222fa858c94ae-ssl" │ │ │
│         │ │          │ │  8 │ accept-ranges             │ bytes                                  │ │ │
│         │ │          │ │  9 │ strict-transport-security │ max-age=31536000                       │ │ │
│         │ │          │ │ 10 │ date                      │ Fri, 14 Mar 2025 06:31:00 GMT          │ │ │
│         │ │          │ ╰────┴───────────────────────────┴────────────────────────────────────────╯ │ │
│         │ ╰──────────┴─────────────────────────────────────────────────────────────────────────────╯ │
│ body    │ Nushell rocks!                                                                             │
│ status  │ 200                                                                                       
╰─────────┴────────────────────────────────────────────────────────────────────────────────────────────╯
>

To get the response status you can retrieve the value from the status field that is in the table:

 
1
2
3
4
5
6
7
# With the --full option a table is returned by http get with the fields
# headers, body and status.
 
# Using get to get the value of the status field.
assert equal (http get --full https://www.mrhaki.com/nushell.txt | get status) 200
 
# Using select to get a record with the result.
assert equal (http get -f https://www.mrhaki.com/nushell.txt | select status) {status: 200}

Written with Nushell 0.102.0.

March 11, 2025

Nushell Niceties: Trimming Strings

Nushell has some powerful commands to work with strings. The str trim command can be used to trim whitespace from a string. By default whitespace from the beginning and end of a string is removed. To only remove whitespace from the beginning of the string you can use the --left argument (or the shorter option -l). For removing whitespace from the end of the string you can use the option --right or the shorter option -r. To trim a string based on another character you can use the option --char or the shorter option -c followed by the character to trim.

In the following example you can see how the str trim command can be used to remove whitespace:

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
use std/assert
 
# str trim removes whitespace from the beginning and end of the string
assert equal (' mrhaki ' | str trim) 'mrhaki'
 
# Use --left or -l to only remove whitespace from the left.
assert equal (' mrhaki ' | str trim --left) 'mrhaki '
 
# Use --right or -r to only remove whitespace from the right.
assert equal (' mrhaki ' | str trim --right) ' mrhaki'
 
# With double quoted strings sr trim removes not
# only spaces, but also other characters like \t and \n.
assert equal ("\tmrhaki\n" | str trim) 'mrhaki'
assert equal ("\t mrhaki \n" | str trim) 'mrhaki'

You can use the argument --char or -c to specify another character to use for trimming:

 
1
2
3
4
5
use std/assert
 
# The str trim command with the --char or -c argument
# to specify a character to trim.
assert equal ('**mrhaki**' | str trim --char '*') 'mrhaki'
assert equal ('* mrhaki *' | str trim -c '*') ' mrhaki '

You can specify the name of keys of a record to trim the values of those keys:

 
1
2
3
4
5
6
7
8
9
10
11
12
use std/assert
 
# The str trim command accepts names of keys to
# trim the values of those keys in a record.
let user = {
    name: 'Hubert Klein Ikkink ',
    alias: ' mrhaki '
} | str trim name alias
 
assert equal $user {
    name: 'Hubert Klein Ikkink',
    alias: 'mrhaki'
}

The str trim command can be applied to lists to trim each element as you can see in the next example:

 
1
2
3
4
5
use std/assert
 
# The str trim command can be applied to lists
# to trim each element.
let shells = [' NuShell', 'Powershell '] | str trim
assert equal $shells ['NuShell', 'Powershell']

Finally the str trim command can be used to trim the values in one or more columns in a table:

 
1
2
3
4
5
6
7
8
9
10
11
12
use std/assert
 
# The str trim command also accepts names of columns for a table.
# The values in the column are trimmed.
let users = [
    [name alias];
    ['Hubert Klein Ikkink ' 'mrhaki ']
] | str trim --right alias
 
# Name column is not transformed.
assert equal $users.0.name 'Hubert Klein Ikkink '
# Alias column is transformed.
assert equal $users.0.alias 'mrhaki'

Written with Nushell 0.102.0.

March 2, 2025

Nushell Niceties: Getting The Current Version Of Nushell

The version command can be used to get the current version of Nushell. The result is a record structure with different keys with information about the Nushell version. For example the property version contains the current version of Nushell, the properties major, minor and patch contain the major, minor and patch version of Nushell.

The version command accepts the argument check to check if a newer version of Nushell is available. The returned record structure contains the property current with a value true or false depending on whether a newer version is available. The version check command also return the latest version as value of the property latest.

In the following example you see output of the version command:

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
> version
╭────────────────────┬────────────────────────────────────────────────╮
│ version            │ 0.102.0                                       
│ major              │ 0                                             
│ minor              │ 102                                           
│ patch              │ 0                                             
│ branch             │                                                │
│ commit_hash        │                                                │
│ build_os           │ macos-x86_64                                   │
│ build_target       │ x86_64-apple-darwin                            │
│ rust_version       │ rustc 1.84.1 (e71f9a9a9 2025-01-27) (Homebrew) │
│ cargo_version      │ cargo 1.84.1                                  
│ build_time         │ 2025-02-04 15:49:35 +00:00                    
│ build_rust_channel │ release                                        │
│ allocator          │ mimalloc                                       │
│ features           │ default, sqlite, trash                         │
│ installed_plugins  │                                                │
╰────────────────────┴────────────────────────────────────────────────╯

You can also use all record navigation options to get specific information from the version command output:

 
1
2
3
> version | get version
0.102.0
> version | get features
default, sqlist, trash

The following example output of the version check command shows the latest version of Nushell is used:

 
1
2
3
4
5
> version check
╭─────────┬─────────╮
│ channel │ release │
│ current │ true    │
│ latest  │ 0.102.0
╰─────────┴─────────╯

The following example uses the if command to check if the current version of Nushell is the latest version:

 
1
2
3
4
5
# Check if the current property of version check is true.
> if (version check).current {
    let v = version
    print $'You are using the latest version of Nushell -> ($v.version)'
}
You are using the latest version of Nushell -> 0.102.0

Written with Nushell 0.102.0.

February 27, 2025

Nushell Niceties: Converting Strings To Kebab Case

Nushell has a lot of commands to work with strings. The str kebab-case command can be used to convert a string to kebab case. Kebab-case is a string that contains only lowercase letters and words are separated by hyphens.

In the following example you can see how to use the str kebab-case command for a string value:

 
1
2
3
4
5
6
7
8
9
10
11
# Add assertion support.
use std/assert
 
# Different examples of string types that are transformed to kebab-case.
assert equal ('stringInCamelCase' | str kebab-case) 'string-in-camel-case'
assert equal ('string_with_underscores' | str kebab-case) 'string-with-underscores'
assert equal ('StringInPascalCase' | str kebab-case) 'string-in-pascal-case'
assert equal ('string with spaces' | str kebab-case) 'string-with-spaces'
assert equal ('STRING-WITH-UPCASE' | str kebab-case) 'string-with-upcase'
 
# Special characters are removed.
assert equal ('Nushell rocks!' | str kebab-case) 'nushell-rocks'

The str kebab-case command accepts as extra argument one or more names of keys in a record. The values of these keys are transformed to kebab-case as you can see in the following example:

 
1
2
3
4
5
6
7
8
9
10
11
12
use std/assert
 
# The str kebab-case command accepts names of keys to
# transform the values to kebab-case in a record.
let user = {
    name: 'Hubert Klein Ikkink',
    alias: 'MrHaki'
} | str kebab-case name alias
 
assert equal $user {
    name: 'hubert-klein-ikkink',
    alias: 'mr-haki'
}

The str kebab-case command can be applied to lists to transform each element to kebab-case:

 
1
2
3
4
5
use std/assert
 
# The str kebab-case command can be applied to lists
# to transform each element to kebab-case.
let shells = ['Nushell' 'PowerShell'] | str kebab-case
assert equal $shells ['nushell' 'power-shell']

Finally the str kebab-case command can be used to transform the values in a column to kebab-case. The names of the columns must be passed as extra arguments to the str kebab-case command:

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
use std/assert
 
# The str kebab-case command also accepts names of columns for a table.
# The values in the column are transformed to kebab-case.
let users = [
    [name alias];
    ['Hubert Klein Ikkink' 'MrHaki']
] | str kebab-case alias
 
# Values in the alias column are transformed into kebab-case.
assert equal $users.alias ['mr-haki']
# Values in the name column are not changed.
assert equal $users.name ['Hubert Klein Ikkink']
# Assertion for complete table.
assert equal $users [[name alias]; ['Hubert Klein Ikkink' 'mr-haki']]

Written with Nushell 0.102.0.

February 20, 2025

Helidon SE Helpings: Serving Observe Endpoints On Different Port

When you enable the /observe endpoints you can configure them to be served on a different port than the application. By default the endpoints are available on the same port as the application. But you can define an extra named socket with another port number in the configuration of the WebServer instance. And in the configuration of the ObserveFeature instance you can define the socket name that should be used for the observe endpoints.

You can use configuration properties or code to configure the extra socket.

Using configuration

In the following example configuration properties file you see how to configure the extra socket and the observe feature to use the extra socket. A new socket with the name observe is defined and port 8081 is used by defining the property prefixed with server.sockets. The value is a list so you have to use a list definition for the property names name and port. Next this socket is used in the configuration of the observe feature by assigning the name to the server.features.observe.sockets property.

 
1
2
3
4
5
6
7
8
9
10
11
12
13
# File: application.properties
...
# Configure new socket for observe endpoints.
# Set the name of the new socket to observe.
server.sockets.0.name=observe
# Set the port for the new socket to 8081.
server.sockets.0.port=8081
 
# Configure the observe feature to use the observe socket.
server.features.observe.sockets.0=observe
 
# Set port for default socket.
server.port=8080
...

In your application code you can use the configuration to create a WebServer instance:

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// File: Main.java
...
import io.helidon.config.Config;
import io.helidon.webserver.WebServer;
...
  public static void main(String[] args) {
    ...
    Config config = Config.create();
 
    WebServer server =
        WebServer.builder()
            ...
            // Read configuration properties.
            .config(config.get("server"))
            .build()
            .start();
  }
...

Using application code

Alternatively you can write the configuration in your application code. In the following example you can see how to create a WebServer instance using the configuration. To add a new socket definition you can use the WebServer.Builder.putSocket() method. Next you can use the ObserveFeature.Builder.sockets() method to connect the observe feature to the new socket.

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// File: Main.java
...
import io.helidon.webserver.observe.ObserveFeature;
import io.helidon.webserver.observe.health.HealthObserver;
import io.helidon.webserver.WebServer;
...
  public static void main(String[] args) {
    ...
    // Create health check observer and enable details.
    HealthObserver healthObserver =
        HealthObserver.builder()
            // Show details in health endpoint.
            .details(true)
            .build();
 
    WebServer server =
        WebServer.builder()
            ...
            // Set default port.
            .port(8080)
            // Create a new socket with name "observe"
            // to run on port 8081.
            .putSocket("observe", socket -> socket.port(8081))
            .addFeature(
                ObserveFeature.builder()
                    // Set name of socket as defined in the WebServer builder.
                    // This will enable the /observe endpoints on port 8081.
                    .sockets(List.of("observe"))
                    .addObserver(healthObserver)
                    .build())
            .build()
            .start();
  }
...

When you start your application you must use port 8081 to access /observe endpoints.

 
1
2
3
4
5
$ curl -X GET http://localhost:8081/observe/health | jq -r .
{
  "status": "UP",
  "checks": []
}
$

Written with Helidon 4.1.6.

February 17, 2025

Helidon SE Helpings: Add Git Information To Info Endpoint

In a previous post you learned how to add information to the /observe/info endpoint. You can also add Git information to the endpoint. For example you can add the Git commit id so you can see check, when the application is running in a production environment, which Git commit for the code was deployed. In order to achieve this you must first generate a properties file with all Git information. The next step is to process this file in your Helidon SE application and add the properties to the /observe/info endpoint.

Define using default configuration

First you need to add the git-commit-id-maven-plugin Maven plugin to your pom.xml file. This plugin can generate a properties file (git.properties) with Git information. In order to generate property names that can be automatically picked up by Helidon you must add the prefix server.features.observe.observers.info.values.git. The Maven plugin will generate a lot of information. You can define regular expressions for property names that should be included in the generated git.properties file. The default location of the generated file is ${project.build.outputDirectory}/git.properties. This means it will be in the root of the classpath of your application.

In the following snippet you can see the configuration of the Maven plugin:

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<!-- File: pom.xml -->
...
<plugin>
    <groupId>io.github.git-commit-id</groupId>
    <artifactId>git-commit-id-maven-plugin</artifactId>
    <version>${git-commit-id-maven-plugin.version}</version>
    <executions>
        <execution>
            <id>get-git-info</id>
            <goals>
                <goal>revision</goal>
            </goals>
        </execution>
    </executions>
    <configuration>
        <!-- Enable generation of file
             ${project.build.outputDirectory}/git.properties
             with Git properties. -->
        <generateGitPropertiesFile>true</generateGitPropertiesFile>
        <!-- Set prefix so Helidon can include the
             generated properties automatically. -->
        <prefix>server.features.observe.observers.info.values.git</prefix>
        <!-- Include all properties that match the following patterns. -->
        <includeOnlyProperties>
            <includeOnlyProperty>.*git.branch$</includeOnlyProperty>
            <includeOnlyProperty>.*git.commit.(time|id)$</includeOnlyProperty>
        </includeOnlyProperties>
    </configuration>
</plugin>
...

When you run the Maven compile phase a new git.properties file is generated. The following shows an example of the contents of this properties file:

 
1
2
3
#Generated by Git-Commit-Id-Plugin
server.features.observe.observers.info.values.git.branch=main
server.features.observe.observers.info.values.git.commit.id=68eb0e361ff27dea59e34c40719fa84125fc46cb
server.features.observe.observers.info.values.git.commit.time=2025-02-15T11\:26\:14+01\:00

To include this information in the output of the /observer/info endpoint you must first include the following dependency so Helidon enables this endpoint:

 
1
2
3
<dependency>
    <groupId>io.helidon.webserver.observe</groupId>
    <artifactId>helidon-webserver-observe-info</artifactId>
</dependency>

Next you must add the generated git.properties file as a source of configuration properties using the Config.builder().addSource() method. Then you can use the configuration to create a WebServer instance. The properties prefixed with server.features.observe.observers.info.values will be picked up by Helidon automatically and added to the info endpoint. This includes the properties from git.properties.

In the following code example you can see how to create a WebServer instance using the configuration:

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// File: Main.java
...
import io.helidon.webserver.WebServer;
import io.helidon.config.Config;
...
  public static void main(String[] args) {
    ...
    Config config =
        Config.builder()
            // Support environment variables and system properties.
            .metaConfig()
            .sources(
              // Use the application.properties file from the classpath.
              ConfigSources.classpath("application.properties"),
               
              // Use the git.properties file from the classpath.
              // This file is created by the git-commit-id-maven-plugin
              // Maven plugin.
              ConfigSources.classpath("git.properties"))
               
            .build();
 
    WebServer server =
        WebServer.builder()
            ...
            // Read configuration properties.
            // This will use all properties with the prefix
            // "server.features.observe.observers.info.values."
            // for the /observe/info endpoint.
            .config(config.get("server"))
            .build()
            .start();
  }
...

If you start the application and go to the /observe/info endpoint you should see the following output:

 
1
2
3
4
5
6
7
8
$ curl  -s -X GET --location "http://localhost:8080/observe/info" \
    -H "Accept: application/json" | jq -r .
{
  "git.branch": "main",
  "git.commit.time": "2025-02-15T11:26:14+01:00",
  "app.name": "blog-sample",
  "git.commit.id": "68eb0e361ff27dea59e34c40719fa84125fc46cb",
  "app.version": "0.0.0-SNAPSHOT"
}

Define using code

Instead of using the default configuration and a prefix of server.features.observe.observers.info.values.git you can also define the logic to define to read the git.properties file in your code. Once the properties are available they can be added to a InfoObserver instance.

First you can remove the prefix option in our pom.xml for the plugin configuration and generate all property names and values as we will filter these in code:

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!-- File: pom.xml -->
...
<plugin>
    <groupId>io.github.git-commit-id</groupId>
    <artifactId>git-commit-id-maven-plugin</artifactId>
    <version>${git-commit-id-maven-plugin.version}</version>
    <executions>
        <execution>
            <id>get-git-info</id>
            <goals>
                <goal>revision</goal>
            </goals>
        </execution>
    </executions>
    <configuration>
        <!-- Enable generation of file
             ${project.build.outputDirectory}/git.properties
             with Git properties. -->
        <generateGitPropertiesFile>true</generateGitPropertiesFile>
    </configuration>
</plugin>
...

Next you must add code to read the git.properties file and add the properties to an InfoObserver instance. In the following code example you can see how this can be done:

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// File: Main.java
...
import io.helidon.webserver.observe.health.HealthObserver;
import io.helidon.webserver.observe.info.InfoObserver;
import io.helidon.webserver.WebServer;
...
  public static void main(String[] args) throws IOException {
    ...
    InfoObserver infoObserver =
        InfoObserver.builder()
                    // Add extra key-value pairs for the
                    // /observe/info endpoint based on
                    // git.properties file.
                    .addValues(getGitInfo())
                    // Extra custom properties that will be
                    // added to the /observe/info endpoint.
                    .addValues(Map.of("app.name", "blog-sample",
                                      "app.version", "0.0.0-SNAPSHOT"))
                    .build();
 
    WebServer server =
        WebServer.builder()
            ...
            // Add the info observer to the server.
            .addFeature(ObserveFeature.create(infoObserver))
            .build()
            .start();
  }
 
  /**
   * Read {@code git.properties} file and return a map with a subset of the properties.
   *
   * @return Map with {@code git.branch}, {@code git.commit.id} and {@code git.commit.time}.
   * @throws IOException Error reading {@code git.properties} file.
   */
  private static Map<String, String> getGitInfo() throws IOException {
    Properties props = new Properties();
    props.load(Main.class.getResourceAsStream("/git.properties"));
    return Map.of(
        "git.branch", props.getProperty("git.branch"),
        "git.commit.id", props.getProperty("git.commit.id"),
        "git.commit.time", props.getProperty("git.commit.time"));
  }
...

Written with Helidon 4.1.6.

February 11, 2025

Helidon SE Helpings: Adding Information To Info Endpoint

It is possible to add an endpoint to Helidon SE that can show information about the application. You can add custom information to this endpoint. In order to enable the endpoint you need to add the dependency io.helidon.webserver.observe:helidon-webserver-observe-info to your pom.xml file. This will add the endpoint /observe/info to your application. You can add key-value pairs to your configuration or code that will be exposed in the endpoint.

To start using the endpoint you must add the following dependency to your pom.xml file:

 
1
2
3
<dependency>
    <groupId>io.helidon.webserver.observe</groupId>
    <artifactId>helidon-webserver-observe-info</artifactId>
</dependency>

You can define the information that should be exposed by the endpoint in your configuration file. Any property name and value prefixed with server.features.observe.observers.info.values. will be added to the endpoint:

 
1
2
3
4
# File: src/main/resources/application.properties
...
server.features.observe.observers.info.values.version=1.0.2
server.features.observe.observers.info.values.app-name=Helidon-Sample
...

Instead of using fixed values for the properties you can also use values from properties coming from the pom.xml file. You can refer to properties name using @{property}@ or ${property} syntax. You must enable resource filtering in your pom.xml:

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
...
    <build>
        ...
        <resources>
            <resource>
                <directory>src/main/resources</directory>
                <filtering>true</filtering>
                <includes>
                    <include>application.properties</include>
                </includes>
            </resource>
        </resources
        ...
    </build>
...

So for our previous example you can also use the POM project properties project.version and project.artifactId in the application.properties file. When the file is copied to the target directory by Maven the placeholders will be replaced with the actual values.

 
1
2
3
4
# File: src/main/resources/application.properties
...
server.features.observe.observers.info.values.version=@project.version@
server.features.observe.observers.info.values.app-name=@project.artifactId@
...

Next you need to read the configuration file and use it to configure a WebServer instance:

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import io.helidon.config.Config;
import io.helidon.webserver.WebServer;
...
  public static void main(String[] args) {
    ...
    Config config = Config.create();
 
    WebServer server =
        WebServer.builder()
            ...
            .config(config.get("server"))
            .build()
            .start();
  }
...

Instead of defining the properties for the /observe/info endpoint in a properties file you can also programmatically define them in your code. You must create a InfoObserver object and add the properties to the instance using the addValues method. This InfoObserver object can be assigned as ObserverFeature to a WebServer instance. In the following example you can see how to define a version and app-name property with some hardcoded value in the application code.

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import io.helidon.webserver.WebServer;
import io.helidon.webserver.observe.ObserveFeature;
import io.helidon.webserver.observe.info.InfoObserver;
import java.util.Map;
...
  public static void main(String[] args) {
    ...
    // Create InfoObserver instance and add properties version and app-name.
    InfoObserver infoObserver =
        InfoObserver.builder()
            .addValues(Map.of("version", "1.0.0", "app-name", "Helidon-Sample"))
            .build();
 
    WebServer server =
        WebServer.builder()
            ...
            // Add observe feature with info observer.
            .addFeature(ObserveFeature.create(infoObserver))
            .build()
            .start();
  }
...

If you start your Helidon SE application you can see the information in the /observe/info endpoint. In the following example we use a curl command to get the information:

 
1
2
3
4
5
$ curl -s -X GET --location "http://localhost:8080/observe/info" | jq .
{
  "app-name": "Helidon-Sample",
  "version": "1.0.0"
}
$

Written with Helidon SE 4.1.6.

February 8, 2025

Helidon SE Helpings: Return Response Based On Request Accept Header

Suppose you want to return a response based on the Accept header of the request. If the Accept header is application/json you want to return a JSON response and if the Accept header is application/xml you want to return an XML response. You can use the isAccepted(MediaType) of the ServerRequestHeaders class to check if the value of the request header Accept is equal to the specified media type. The method returns true if the media type is defined for the request header Accept and false if not.

In order to convert an object to XML you need to add the dependency com.fasterxml.jackson.dataformat:jackson-dataformat-xml to your pom.xml file:

 
1
2
3
<dependency>
    <groupId>com.fasterxml.jackson.dataformat</groupId>
    <artifactId>jackson-dataformat-xml</artifactId>
</dependency>

In the following example HttpService implementation the request header Accept is used to determine if the response should be in XML or default JSON format:

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package mrhaki;
 
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import io.helidon.common.media.type.MediaTypes;
import io.helidon.http.HeaderNames;
import io.helidon.webserver.http.HttpRules;
import io.helidon.webserver.http.HttpService;
import io.helidon.webserver.http.ServerRequest;
import io.helidon.webserver.http.ServerResponse;
 
/** Example service to return data in either XML or JSON depending on the request header Accept. */
class SampleService implements HttpService {
  /** Use for converting an object to XML. */
  private final XmlMapper xmlMapper = new XmlMapper();
 
  @Override
  public void routing(HttpRules rules) {
    rules.get("/content", this::handle);
  }
 
  private void handle(ServerRequest req, ServerResponse resp) throws JsonProcessingException {
    // Define record for data structure to send in the response.
    record Message(String content) {}
 
    // Check if the Accept header is set with the value application/xml.
    if (req.headers().isAccepted(MediaTypes.APPLICATION_XML)) {
      // Return XML response.
      resp.header(HeaderNames.CONTENT_TYPE, MediaTypes.APPLICATION_XML.text())
          .send(xmlMapper.writeValueAsString(new Message("Sample")));
    } else {
      // Return default JSON response.
      resp.send(new Message("Sample"));
    }
  }
}

When you access the endpoint with different values for the request header Accept you get a response in the format you specified:

 
1
2
3
4
5
6
$ curl -s -X GET --location "http://localhost:8080/sample/content" \
    -H "Accept: application/xml" | xmllint --format -
<?xml version="1.0">
<Message>
  <content>Sample</content>
</Message>
$
 
1
2
3
4
5
$ curl -s -X GET --location "http://localhost:8080/sample/content" \
    -H "Accept: application/json" | jq -r .
{
  "content": "Sample"
}
$

Written with Helidon SE 4.1.6.

Spring Sweets: Use Logging in EnvironmentPostProcessor Implementation

You can write an implementation of the interface EnvironmentPostProcessor to customize the Environment of a Spring Boot application. For example you can read an external configuration file and add its properties to the Environment. If you want to add some logging statement to the class then you need to make sure you pass a DeferredLogFactory to the constructor of the class. From this factory you can use the getLogger method to get a Log instance. This is needed, because the implementation of the interface EnvironmentPostProcessor is created before the logging system is initialized. By using the Log instances created from DeferredLogFactory Spring Boot will make sure the log messages are written when the logging system is initialized.

In the following example you can see how to use the DeferredLogFactory:

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package mrhaki.sample;
 
import org.apache.commons.logging.Log;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.env.EnvironmentPostProcessor;
import org.springframework.boot.logging.DeferredLogFactory;
import org.springframework.core.annotation.Order;
import org.springframework.core.env.ConfigurableEnvironment;
 
/**
 * Sample implementation for {@link EnvironmentPostProcessor}.
 */
@Order
public class SampleEnvironmentPostProcessor implements EnvironmentPostProcessor {
 
  /**
   * Log instance to log messages after the logging system is initialized.
   */
  private final Log log;
 
  /**
   * Create instance.
   *
   * @param logFactory Use logFactory to create logger that will output if the logging system is
   *     initialized.
   */
  public SampleEnvironmentPostProcessor(DeferredLogFactory logFactory) {
    log = logFactory.getLog(SampleEnvironmentPostProcessor.class);
  }
 
  @Override
  public void postProcessEnvironment(
      ConfigurableEnvironment environment, SpringApplication application) {
    if (log.isInfoEnabled()) {
      // This log message will only be written once the logging system is initialized
      // and the log level is INFO or higher.
      log.info("Sample environment post processor started");
    }
  }
}

The support for DeferredLogFactory is added in Spring Boot 2.4.

Written with Spring Boot 3.4.2.