Oicana

Java using Spring Boot

In this chapter, you’ll integrate Oicana into a Java web service using Spring Boot. Spring Boot is a popular Java framework for building production-ready web services. We’ll create a simple web service that compiles your Oicana template to PDF and serves it via an HTTP endpoint.


Note

This chapter assumes that you have a working Java 17+ setup with Gradle. If that is not the case, please follow the Adoptium installation guide to install a JDK and install Gradle. Make sure your Gradle version supports your Java version.


Let’s start with a fresh Spring Boot project. Create a new directory and initialize it with Gradle:

mkdir my-pdf-service
cd my-pdf-service
gradle init --type basic --dsl kotlin
mkdir my-pdf-service
cd my-pdf-service
gradle init --type basic --dsl kotlin

Replace the generated build.gradle.ktsbuild.gradle.kts with:


plugins {
java
id("org.springframework.boot") version "3.4.3"
id("io.spring.dependency-management") version "1.1.7"
}
group = "com.example"
version = "1.0.0"
java {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
repositories {
mavenCentral()
}
dependencies {
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("com.oicana:oicana:0.1.0-alpha.2")
// Add the native library for your platform.
// You can add multiple if your team uses different platforms.
runtimeOnly("com.oicana:oicana-linux-x86_64:0.1.0-alpha.2")
}
plugins {
java
id("org.springframework.boot") version "3.4.3"
id("io.spring.dependency-management") version "1.1.7"
}
group = "com.example"
version = "1.0.0"
java {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
repositories {
mavenCentral()
}
dependencies {
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("com.oicana:oicana:0.1.0-alpha.2")
// Add the native library for your platform.
// You can add multiple if your team uses different platforms.
runtimeOnly("com.oicana:oicana-linux-x86_64:0.1.0-alpha.2")
}

And set the project name in settings.gradle.ktssettings.gradle.kts:


rootProject.name = "my-pdf-service"
rootProject.name = "my-pdf-service"


Note

Replace the runtimeOnlyruntimeOnly dependency with the native library for your platform. Available options: oicana-linux-x86_64oicana-linux-x86_64, oicana-linux-aarch64oicana-linux-aarch64, oicana-macos-x86_64oicana-macos-x86_64, oicana-macos-aarch64oicana-macos-aarch64, oicana-windows-x86_64oicana-windows-x86_64. You can add multiple if your team uses different platforms - only the matching one will be loaded at runtime.

Create the main application class:


package com.example;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
package com.example;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}

You can test it by running ./gradlew bootRun./gradlew bootRun and navigating to http://localhost:8080 in your browser.

New service endpoint

We will define a new endpoint to compile our Oicana template to a PDF and return the PDF file to the user.


  1. Create a new directory in the project called templatestemplates and copy example-0.1.0.zipexample-0.1.0.zip into that directory.
  2. Create a service to load and compile the template:


    package com.example;
    import com.oicana.CompilationMode;
    import com.oicana.ExportFormat;
    import com.oicana.Template;
    import jakarta.annotation.PostConstruct;
    import jakarta.annotation.PreDestroy;
    import org.springframework.stereotype.Service;
    import java.io.IOException;
    import java.nio.file.Files;
    import java.nio.file.Path;
    import java.util.Map;
    @Service
    public class TemplateService {
    private Template template;
    @PostConstruct
    public void init() throws IOException {
    byte[] templateBytes = Files.readAllBytes(
    Path.of("templates/example-0.1.0.zip")
    );
    template = new Template(templateBytes);
    }
    public byte[] compile() {
    return template.compile(
    ExportFormat.pdf(),
    CompilationMode.DEVELOPMENT
    );
    }
    @PreDestroy
    public void cleanup() {
    template.close();
    }
    }
    package com.example;
    import com.oicana.CompilationMode;
    import com.oicana.ExportFormat;
    import com.oicana.Template;
    import jakarta.annotation.PostConstruct;
    import jakarta.annotation.PreDestroy;
    import org.springframework.stereotype.Service;
    import java.io.IOException;
    import java.nio.file.Files;
    import java.nio.file.Path;
    import java.util.Map;
    @Service
    public class TemplateService {
    private Template template;
    @PostConstruct
    public void init() throws IOException {
    byte[] templateBytes = Files.readAllBytes(
    Path.of("templates/example-0.1.0.zip")
    );
    template = new Template(templateBytes);
    }
    public byte[] compile() {
    return template.compile(
    ExportFormat.pdf(),
    CompilationMode.DEVELOPMENT
    );
    }
    @PreDestroy
    public void cleanup() {
    template.close();
    }
    }

    The TemplateTemplate constructor loads the template once. The compilecompile method compiles it without inputs and CompilationMode.DEVELOPMENTCompilationMode.DEVELOPMENT, so the template uses the development value you defined for the infoinfo input ({ "name": "Chuck Norris" }{ "name": "Chuck Norris" }). In a follow-up step, we will set an input value instead. The @PreDestroy@PreDestroy cleanup releases native resources.


  3. Create a controller with a compile endpoint:

    package com.example;
    import org.springframework.http.HttpHeaders;
    import org.springframework.http.MediaType;
    import org.springframework.http.ResponseEntity;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RestController;
    @RestController
    public class CompileController {
    private final TemplateService templateService;
    public CompileController(TemplateService templateService) {
    this.templateService = templateService;
    }
    @PostMapping("/compile")
    public ResponseEntity<byte[]> compile() {
    byte[] pdf = templateService.compile();
    return ResponseEntity.ok()
    .header(HttpHeaders.CONTENT_DISPOSITION,
    "attachment; filename=\"example.pdf\"")
    .contentType(MediaType.APPLICATION_PDF)
    .body(pdf);
    }
    }
    package com.example;
    import org.springframework.http.HttpHeaders;
    import org.springframework.http.MediaType;
    import org.springframework.http.ResponseEntity;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RestController;
    @RestController
    public class CompileController {
    private final TemplateService templateService;
    public CompileController(TemplateService templateService) {
    this.templateService = templateService;
    }
    @PostMapping("/compile")
    public ResponseEntity<byte[]> compile() {
    byte[] pdf = templateService.compile();
    return ResponseEntity.ok()
    .header(HttpHeaders.CONTENT_DISPOSITION,
    "attachment; filename=\"example.pdf\"")
    .contentType(MediaType.APPLICATION_PDF)
    .body(pdf);
    }
    }

    This code defines a new POST endpoint at /compile/compile. For every request, it compiles the template and returns the PDF file.

After restarting the service, you can test the endpoint with curl:

curl -X POST http://localhost:8080/compile --output example.pdf
curl -X POST http://localhost:8080/compile --output example.pdf

The generated example.pdfexample.pdf file should contain your template with the development value.

About performance

The PDF generation should not take longer than a couple of milliseconds. The TemplateTemplate instance is thread-safe and can be shared across requests - Spring Boot’s singleton service scope handles this naturally.

Passing inputs from Java

Our compilecompile method is currently calling template.compile()template.compile() without inputs in development mode. Now we’ll provide an explicit input value and switch to production mode:

public byte[] compile() {
return template.compile(
Map.of("info", "{\"name\": \"Baby Yoda\"}"),
Map.of()
);
}
public byte[] compile() {
return template.compile(
Map.of("info", "{\"name\": \"Baby Yoda\"}"),
Map.of()
);
}


We now pass JSON inputs and an empty blob inputs map. The compile(Map, Map)compile(Map, Map) overload defaults to CompilationMode.PRODUCTIONCompilationMode.PRODUCTION and PDF output. Production mode is the recommended default for all document compilation in your application - it ensures you never accidentally generate a document with test data. In production mode, the template will never fall back to development values for inputs. If an input value is missing in production mode and the input does not have a default value, the compilation will fail unless your template handles nonenone values for that input.


Calling the endpoint now will result in a PDF with “Baby Yoda” instead of “Chuck Norris”. Building on this minimal service, you could set input values based on database entries or the request payload. Take a look at the open source Spring Boot example project on GitHub for a more complete showcase of the Oicana Java integration.