Core

Module hexagon_core

This module holds utilities used in other libraries of the toolkit. Check the packages' documentation for more details. You can find a quick recap of the main features in the sections below.

Install the Dependency

This module is not meant to be imported directly. It will be included by using any other part of the toolkit. However, if you only want to use the utilities, logging or dependency injection (i.e.: for a desktop application), you can import it with the following code:

1
2
3
4
5
repositories {
    mavenCentral()
}

implementation("com.hexagonkt:hexagon_core:$hexagonVersion")
1
2
3
4
5
<dependency>
  <groupId>com.hexagonkt</groupId>
  <artifactId>hexagon_core</artifactId>
  <version>$hexagonVersion</version>
</dependency>

Logger

The following code block shows the most common use cases for the Logger class:

 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
val classLogger: Logger = Logger(Runtime::class) // Logger for the `Runtime` class
val instanceLogger: Logger = Logger(this) // Logger for this instance's class

logger.info {
    """
    You can add a quick log without declaring a Logger with 'com.hexagonkt.helpers.logger'.
    It is a default logger created for the System class (same as `Logger(System::class)`).
    """
}

classLogger.trace { "Message only evaluated if trace enabled at ${Jvm.id}" }
classLogger.debug { "Message only evaluated if debug enabled at ${Jvm.id}" }
classLogger.warn { "Message only evaluated if warn enabled at ${Jvm.id}" }
classLogger.info { "Message only evaluated if info enabled at ${Jvm.id}" }

val exception = IllegalStateException("Exception")
classLogger.warn(exception) { "Warning with exception" }
classLogger.error(exception) { "Error message with exception" }
classLogger.error { "Error without an exception" }

classLogger.time("Logs the time used to run the following block of code") {
    val message = "Block of code to be timed"
    assert(message.isNotBlank())
}

instanceLogger.flare { "Prints a log that stands out for ease searching" }

Hexagon uses the SLF4J logging library, you can use any of its implementations by just adding the library to your classpath. Below you can see some alternatives:

1
2
3
4
5
6
    /*
     * Pick ONLY ONE of the options below
     */
    implementation("ch.qos.logback:logback-classic:1.2.3") // Full featured implementation
    implementation("org.slf4j:slf4j-simple:1.7.30") // Lightweight logging implementation
    implementation("org.slf4j:slf4j-jdk14:1.7.30") // Uses JDK logging API
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
<!--
 ! Pick ONLY ONE of the options below
 !-->
<!-- Full featured implementation -->
<dependency>
  <groupId>ch.qos.logback</groupId>
  <artifactId>logback-classic</artifactId>
  <version>1.2.3</version>
</dependency>
<!-- Lightweight logging implementation -->
<dependency>
  <groupId>org.slf4j</groupId>
  <artifactId>slf4j-simple</artifactId>
  <version>1.7.30</version>
</dependency>
<!-- Uses JDK logging API -->
<dependency>
  <groupId>org.slf4j</groupId>
  <artifactId>slf4j-jdk14</artifactId>
  <version>1.7.30</version>
</dependency>

Info

You can bridge other logging libraries (that may be used by other third party libraries you use importing adaptor libraries (check SLF4J bridge guide for details). For example:

1
2
3
4
    // Bridges
    runtimeOnly("org.slf4j:jcl-over-slf4j:1.7.30")
    runtimeOnly("org.slf4j:log4j-over-slf4j:1.7.30")
    runtimeOnly("org.slf4j:jul-to-slf4j:1.7.30") // Don't add it if you are using 'slf4j-jdk14'
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
<dependency>
  <groupId>org.slf4j</groupId>
  <artifactId>jcl-over-slf4j</artifactId>
  <version>1.7.30</version>
</dependency>
<dependency>
  <groupId>org.slf4j</groupId>
  <artifactId>log4j-over-slf4j</artifactId>
  <version>1.7.30</version>
</dependency>
<!-- Don't add the next one if you are using 'slf4j-jdk14' -->
<dependency>
  <groupId>org.slf4j</groupId>
  <artifactId>jul-to-slf4j</artifactId>
  <version>1.7.30</version>
</dependency>

Dependency injection

You can take advantage of dependency injection using the InjectionManager object.

The implementation is a map of classes (with an optional tag) to provider functions (in essence: Map<KClass<*>, () -> Any>). It is a very simple, yet complete, DI implementation.

You can bind supplier functions or objects to classes. If a class is already bound, later calls to bind* methods are ignored. However, you can use the forceBind* methods if you need to override a binding (in tests for example).

Check this sample to bind constructor functions or objects to classes, and inject them later:

 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
// Bind classes to functions (create a different instance with each `inject` call)
InjectionManager.bind<Date> { java.sql.Date(System.currentTimeMillis()) }

// Bind classes to objects (returns the same instance for all `inject` calls)
InjectionManager.bindObject<String>("STR")

// You can use labels to inject different instances
InjectionManager.bindObject<String>("toolkit", "Hexagon")
InjectionManager.bind<Date>("+1h") {
    java.sql.Date(System.currentTimeMillis() + 3_600_000)
}

val currentSqlDate = InjectionManager.inject<Date>()
val currentSqlDateInferredType: Date = InjectionManager.inject()

// Inject different values for a class using tags (can be any type, not only string)
val nextHourSqlDate: Date = InjectionManager.inject("+1h")
val nextHourSqlDateInferredType: Date = InjectionManager.inject("+1h")

// Injecting classes bound to objects return always the same instance
val defaultString = InjectionManager.inject<String>()
val taggedString: String = InjectionManager.inject("toolkit")

// Overriding previously bound classes is not allowed (ignored)
InjectionManager.bindObject<String>("STR Ignored")
val ignoredBinding = InjectionManager.inject<String>()

// You can overwrite previously bound classes using `forceBind*` methods
InjectionManager.forceBindObject<String>("STR Overridden")
val overriddenBinding = InjectionManager.inject<String>()

Info

Dependency Injection is not required by the toolkit. All classes and methods have versions receiving all of their dependencies, so you can use them instead relying on injection (or use another DI library of your choice).

Serialization

The core module has utilities to serialize/parse data classes to JSON and YAML. Read the following snippet for details:

1
2
3
4
5
6
7
val jason = Person("Jason", "Jackson", LocalDate.of(1989, 12, 31))

val jasonJson = jason.serialize(Json) // Can also be Yaml or an string: "application/json"
val parsedJason = jasonJson.parse(Person::class)

assert(jason == parsedJason)
assert(jason !== parsedJason)

Settings

This module helps loading external settings from different sources. You can change the settings sources, the default ones are (bottom sources override top ones):

  1. Resource /application.yml.
  2. Environment variables starting with APPLICATION_.
  3. System properties starting with service.
  4. File ./application.yml from the application run directory.
  5. Resource /application_test.yml.

Below there is a code fragment showing how to add a custom settings source and load its properties:

1
2
3
4
5
6
7
8
9
SettingsManager.settingsSources += ObjectSource(
    "stringProperty" to "str",
    "integerProperty" to 101,
    "booleanProperty" to true
)

assert(SettingsManager.settings["stringProperty"] == "str")
assert(SettingsManager.settings["integerProperty"] == 101)
assert(SettingsManager.settings["booleanProperty"] == true)

Package com.hexagonkt.helpers

JVM information, a logger class and other useful utilities.

Package com.hexagonkt.injection

Utilities to bind classes to creation closures or instances, and inject instances of those classes later.

Package com.hexagonkt.serialization

Parse/serialize data in different formats to class instances.

Package com.hexagonkt.settings

Load settings from different data sources and formats.

Comments