Skip to content

Commit

Permalink
feat: Refactoring and add PDFGenCore initializer method (#10)
Browse files Browse the repository at this point in the history
* Initializer: make it possible to initalize PDFGenCore with additional handlebar helpers

* Update and add handlebar helpers

* Fix tests and linting

* Update json_to_period to support json payload

* Add tests and update README.md

* Change environment variabel to be privat

* Better errorhandling when resource file does not exists
  • Loading branch information
ugur93 authored Nov 22, 2023
1 parent 32f42e8 commit ea1e8bf
Show file tree
Hide file tree
Showing 13 changed files with 508 additions and 99 deletions.
138 changes: 128 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,60 @@ Repository for `pdfgen-core`, an application written in Kotlin used to create PD

## Getting started

Most commonly, pdfgen-core is used with templates, fonts, additional resources, and potential test data to verify that valid PDFs get produced by the aforementioned templates.
pdfgen-core is used with templates, fonts, additional resources, and potential test data to verify that valid PDFs get produced by the templates.

Check GitHub releases to find the latest `release` version
Check [Github releases](https://github.com/navikt/pdfgen-core/releases) to find the latest `release` version

In your own repository create subfolders in `templates` and `data`
```bash
mkdir {templates,data}/directory_name # directory_name can be anything, but it'll be a necessary part of the API later
````
* `templates/directory_name/` should then be populated with your .hbs-templates. the names of these templates will also decide parts of the API paths
* `data/directory_name/` should be populated with json files with names corresponding to a target .hbs-template, this can be used to test your PDFs during development of templates.

Additionally create subfolder `resources` containing additional resources which can be referred in your .hbs-templates
In your own repository create folders `templates`, `resources` and `data` on root of you repository
* `templates`
Create subfolder inside `templates` folder
```bash
mkdir {templates}/directory_name # directory_name can be anything, but it'll be a necessary part of the API later
````
* `templates/directory_name/` should then be populated with your .hbs-templates. the names of these templates will also decide parts of the API paths

* `resources` should contain additional resources (ex images) which can be referred in your .hbs-templates
* `data` should contain test JSON data that can be used to verify that valid PDFs get produced by templates. `data` folder should have same subdirectory structure as `templates`. [Example](#generating-HTML-from-predefined-JSON-data-in-data-folder) how you can produce HTML and PDF with test data


### Initialize pdfgen-core
#### Initialize with custom handlebar helpers
You can initialize pdfgen-core with additional handlebar helpers:
```kotlin
PDFGenCore.init(Environment(additionalHandlebarHelpers = mapOf(
"enum_to_readable" to Helper<String> { context, _ ->
when(context){
"SOME_ENUM_VALUE" -> "Human readable text"
else -> ""
}
},
)))
```
#### Initialize with alternative path to `templates`, `resources` folder
```kotlin
PDFGenCore.init(
Environment(
additionalHandlebarHelpers = mapOf(
"enum_to_readable" to Helper<String> { context, _ ->
when (context) {
"BIDRAGSMOTTAKER" -> "Bidragsmottaker"
else -> ""
}
},
),
templateRoot = PDFGenResource("/path/to/templates"),
resourcesRoot = PDFGenResource("/path/to/resources"),
),
)
```

#### Reload templates and resources from disk
You can reload the resources and templates from disk using the following method.
This will be especially useful for when developing templates and should be executed before generating HTML or PDF such that the updated templates and data are loaded from disk
```kotlin
PDFGenCore.reloadEnvironment()
```

### Example usage
#### Generating HTML from predefined JSON data in data-folder
Expand All @@ -49,11 +89,89 @@ val pdfBytes: ByteArray = createPDFA(html)
val pdfBytes: ByteArray = createPDFA(template, directoryName, jsonNode)
```

## Handlerbars helpers
Example of usage of handlebar helpers defined in this library

### {{filter}}

Filter array of objects by fieldvalue

**Example data**

```json
{
"roller": [
{
"type": "BARN",
"navn": "Barn1 Etternavn"
},
{
"type": "BARN",
"navn": "Barn2 Etternavn"
},
{
"type": "FORELDRE",
"navn": "Mor Etternavn"
}
]
}
```

**Params**

* `json` **{List}** List of JSON values
* `fieldname` **{String}** Fieldname used for filtering
* `value` **{String}** Value to filter by
* `returns` **{List}** Filtered list

**Example**

```handlebars
{{#filter roller "type" "BARN" as |barn|}}
<div>{{ barn.navn }}</div>
{{/filter}}
```

### {{json_to_period}}

Format json with parameters `fom`, `tom`/`til` as period string

**Example data**

```json
{
"periode": {
"fom": "2020-03-20",
"tom": "2021-09-23"
}
}
```

**Params**

* `json` **{Periode}** Object with fields `fom` and `tom` or`til`
* `returns` **{List}** Formatted date (ex `20.03.2020 - 23.09.2021`)

**Example**

```handlebars
{{json_to_period periode}}
```
## Developing pdfgen-core

### Build and run tests
`./gradlew shadowJar`
`./gradlew build`

### Publish to local maven repository
Run the following command
`./gradlew publishToMavenLocal` (or `./gradlew -t publishToMavenLocal` if you want to enable autobuild on changes)

This will publish `pdfgen-core` to local maven repository with version `local-build`

You can then import `pdfgen-core` to your gradle project with

`implementation("no.nav.pdfgen:pdfgen-core:local-build")`
### Upgrading the gradle wrapper
Find the newest version of gradle here: https://gradle.org/releases/ Then run this command:

Expand Down
10 changes: 7 additions & 3 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ val prometheusVersion = "0.16.0"
val junitJupiterVersion = "5.10.1"
val verapdfVersion = "1.24.1"
val ktfmtVersion = "0.44"
val kotlinloggerVesion = "5.1.0"
val kotlinloggerVersion = "5.1.0"
val kotestVersion = "5.8.0"
val javaVersion = JavaVersion.VERSION_21


Expand All @@ -32,7 +33,7 @@ plugins {
tasks {

test {
useJUnitPlatform {}
useJUnitPlatform()
testLogging {
events("passed", "skipped", "failed")
showStackTraces = true
Expand Down Expand Up @@ -80,7 +81,7 @@ dependencies {
implementation("io.prometheus:simpleclient_hotspot:$prometheusVersion")

implementation("org.verapdf:validation-model:$verapdfVersion")
implementation("io.github.oshai:kotlin-logging-jvm:$kotlinloggerVesion")
implementation("io.github.oshai:kotlin-logging-jvm:$kotlinloggerVersion")

implementation("ch.qos.logback:logback-classic:$logbackVersion")
implementation("net.logstash.logback:logstash-logback-encoder:$logstashEncoderVersion")
Expand All @@ -89,6 +90,9 @@ dependencies {
testImplementation("org.junit.jupiter:junit-jupiter-params:$junitJupiterVersion")
testImplementation("org.junit.jupiter:junit-jupiter-engine:$junitJupiterVersion")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
testImplementation("io.kotest:kotest-assertions-core:$kotestVersion")
testImplementation("io.kotest:kotest-property:$kotestVersion")


}

Expand Down
79 changes: 44 additions & 35 deletions src/main/kotlin/no/nav/pdfgen/core/Environment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,8 @@ package no.nav.pdfgen.core
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import com.fasterxml.jackson.module.kotlin.registerKotlinModule
import com.github.jknack.handlebars.Helper
import io.github.oshai.kotlinlogging.KotlinLogging
import no.nav.pdfgen.core.util.FontMetadata
import org.apache.pdfbox.io.IOUtils
import java.io.File
import java.nio.file.Files
import java.nio.file.Path
Expand All @@ -15,35 +14,27 @@ import kotlin.io.path.exists
import kotlin.io.path.extension
import kotlin.io.path.pathString
import kotlin.io.path.readBytes
import no.nav.pdfgen.core.template.loadTemplates
import no.nav.pdfgen.core.util.FontMetadata
import org.apache.pdfbox.io.IOUtils

private val log = KotlinLogging.logger {}
val objectMapper: ObjectMapper = ObjectMapper().findAndRegisterModules().registerKotlinModule()
val templateRoot: PDFGenResource = PDFGenResource("TEMPLATES_PATH", "templates/")
val imagesRoot: PDFGenResource = PDFGenResource("RESOURCES_PATH", "resources/")
val fontsRoot: PDFGenResource = PDFGenResource("FONTS_PATH", "fonts/")

class PDFgen {
companion object {
private var environment = Environment()

fun getEnvironment() = environment

fun init(environment: Environment) {
PDFgen.environment = environment
}
}
}

data class Environment(
val images: Map<String, String> = loadImages(),
val resources: Map<String, ByteArray> = loadResources(),
val colorProfile: ByteArray =
IOUtils.toByteArray(Environment::class.java.getResourceAsStream("/sRGB2014.icc")),
val fonts: List<FontMetadata> =
objectMapper.readValue(fontsRoot.readAllBytes("config.json")),
val disablePdfGet: Boolean = System.getenv("DISABLE_PDF_GET")?.let { it == "true" } ?: false,
val enableHtmlEndpoint: Boolean =
System.getenv("ENABLE_HTML_ENDPOINT")?.let { it == "true" } ?: false,
class Environment(
val additionalHandlebarHelpers: Map<String, Helper<*>> = emptyMap(),
val templateRoot: PDFGenResource = PDFGenResource("templates/"),
val resourcesRoot: PDFGenResource = PDFGenResource("resources/"),
val fontsRoot: PDFGenResource = PDFGenResource("fonts/"),
val dataRoot: PDFGenResource = PDFGenResource("data/"),
) {
val colorProfile: ByteArray =
IOUtils.toByteArray(Environment::class.java.getResourceAsStream("/sRGB2014.icc"))
val images: Map<String, String> = loadImages(resourcesRoot)
val resources: Map<String, ByteArray> = loadResources(resourcesRoot)
val fonts: List<FontMetadata> = objectMapper.readValue(fontsRoot.readAllBytes("config.json"))
val templates = loadTemplates(templateRoot, additionalHandlebarHelpers)

override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
Expand All @@ -56,27 +47,45 @@ data class Environment(
override fun hashCode(): Int {
return colorProfile.contentHashCode()
}

fun copy(): Environment {
return Environment(
additionalHandlebarHelpers = additionalHandlebarHelpers,
templateRoot = templateRoot,
resourcesRoot = resourcesRoot,
fontsRoot = fontsRoot,
dataRoot = dataRoot,
)
}
}

data class PDFGenResource(val envVariableName: String, val defaultPath: String){
data class PDFGenResource(val path: String) {

private val _path: Path = Paths.get(path)

private val _path: Path = System.getenv(envVariableName)?.let { Paths.get(it) }
?: Paths.get(defaultPath)
fun readAllBytes(filename: String? = null): ByteArray {
val filePath = filename?.let { _path.resolve(it) } ?: _path
return if (filePath.exists()) filePath.readBytes() else Environment::class.java.classLoader.getResourceAsStream(filePath.pathString)!!.readAllBytes()
return if (filePath.exists()) filePath.readBytes()
else
Environment::class
.java
.classLoader
.getResourceAsStream(filePath.pathString)!!
.readAllBytes()
}

fun toFile(filename: String? = null): File = getPath(filename).toFile()

fun getPath(filename: String? = null): Path {
val filePath = filename?.let { _path.resolve(it) } ?: _path
log.debug { "Reading file from path $filePath. File exists on path = ${filePath.exists()}" }
return if (filePath.exists()) filePath else Path.of(Environment::class.java.classLoader.getResource(filePath.pathString)!!.toURI())
log.trace { "Reading file from path $filePath. File exists on path = ${filePath.exists()}" }
return if (filePath.exists()) filePath
else Environment::class.java.classLoader.getResource(filePath.pathString)?.let{
Path.of(it.toURI()) } ?: throw RuntimeException("Could not find file at path $filePath")
}
}

private fun loadImages() =
private fun loadImages(imagesRoot: PDFGenResource) =
Files.list(imagesRoot.getPath())
.filter {
val validExtensions = setOf("jpg", "jpeg", "png", "bmp", "svg")
Expand All @@ -97,7 +106,7 @@ private fun loadImages() =
.toList()
.toMap()

private fun loadResources() =
private fun loadResources(imagesRoot: PDFGenResource) =
Files.list(imagesRoot.getPath())
.filter {
val validExtensions = setOf("svg")
Expand Down
1 change: 0 additions & 1 deletion src/main/kotlin/no/nav/pdfgen/core/MetricRegistry.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,3 @@ val OPENHTMLTOPDF_RENDERING_SUMMARY: Summary =
.help("Time it takes to render a PDF")
.labelNames("application_name", "template_type")
.register()

21 changes: 21 additions & 0 deletions src/main/kotlin/no/nav/pdfgen/core/PDFGenCore.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package no.nav.pdfgen.core

import io.github.oshai.kotlinlogging.KotlinLogging
import java.util.concurrent.atomic.AtomicReference

private val coreEnvironment = AtomicReference(Environment())
private val log = KotlinLogging.logger {}

class PDFGenCore {
companion object {
fun init(initialEnvironment: Environment) {
coreEnvironment.set(initialEnvironment)
}

val environment: Environment get() = coreEnvironment.get()
fun reloadEnvironment() {
log.debug { "Reloading environment" }
coreEnvironment.set(coreEnvironment.get().copy())
}
}
}
1 change: 1 addition & 0 deletions src/main/kotlin/no/nav/pdfgen/core/domain/Periode.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ import java.time.LocalDate
class Periode {
val fom: LocalDate? = null
val tom: LocalDate? = null
val til: LocalDate? = null
}
7 changes: 5 additions & 2 deletions src/main/kotlin/no/nav/pdfgen/core/domain/PeriodeMapper.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,11 @@ object PeriodeMapper {

internal fun jsonTilPeriode(json: String): Periode {
try {
val periode = objectMapper.readValue(json, Periode::class.java)
if (periode.tom == null || periode.fom == null || periode.fom.isAfter(periode.tom)) {
val periode = objectMapper.readValue(json, Periode::class.java) ?: return Periode()
if (periode.tom != null && (periode.fom == null || periode.fom.isAfter(periode.tom))) {
throw IllegalArgumentException()
}
if (periode.til != null && (periode.fom == null || periode.fom.isAfter(periode.til))) {
throw IllegalArgumentException()
}
return periode
Expand Down
Loading

0 comments on commit ea1e8bf

Please sign in to comment.