Download an Excel file in Micronaut App

Learn how to download an excel file with Micronaut and Spreadsheet Builder library.

Authors: Sergio del Amo

Micronaut Version: 1.0.0.M4

1 Getting Started

In this guide, we are going to demonstrate Micronaut file transfer capabilities by creating an app which downloads an excel file with a list of books.

If you are using Java or Kotlin and IntelliJ IDEA make sure you have enabled annotation processing.

annotationprocessorsintellij

1.1 What you will need

To complete this guide, you will need the following:

  • Some time on your hands

  • A decent text editor or IDE

  • JDK 1.8 or greater installed with JAVA_HOME configured appropriately

1.2 Solution

We recommend you to follow the instructions in the next sections and create the app step by step. However, you can go right to the completed example.

or

Then, cd into the complete folder which you will find in the root project of the downloaded/cloned project.

2 Writing the App

2.1 Books

Create Book POJO:

src/main/java/example/micronaut/Book.java
package example.micronaut;

import java.util.Objects;

public class Book {
    private String isbn;
    private String name;

    public Book() {}

    public Book(String isbn, String name) {
        this.isbn = isbn;
        this.name =  name;
    }

    public String getIsbn() {
        return isbn;
    }

    public void setIsbn(String isbn) {
        this.isbn = isbn;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Book book = (Book) o;
        return Objects.equals(isbn, book.isbn) &&
                Objects.equals(name, book.name);
    }

    @Override
    public int hashCode() {

        return Objects.hash(isbn, name);
    }
}

Create an interface to encapsulate Book retrieval.

src/main/java/example/micronaut/BookRepository.java
package example.micronaut;

import java.util.List;

public interface BookRepository {

    List<Book> findAll();
}

Create a bean which implements the previous interface

src/main/java/example/micronaut/BookRepositoryImpl.java
package example.micronaut;

import javax.inject.Singleton;
import java.util.List;

import java.util.Arrays;

@Singleton (1)
public class BookRepositoryImpl implements BookRepository {
    @Override
    public List<Book> findAll() {
        Book buildingMicroservices = new Book("1491950358", "Building Microservices");
        Book releaseIt = new Book("1680502395", "Release It!");
        Book cidelivery = new Book("0321601912", "Continuous Delivery:");
        return Arrays.asList(buildingMicroservices, releaseIt, cidelivery);

    }
}
1 To register a Singleton in Micronaut’s application context annotate your class with javax.inject.Singleton

2.2 Spreadsheet Builder

Add a dependency to Spreadsheet builder

Spreadsheet builder provides convenient way how to read and create MS Excel OfficeOpenXML Documents (XSLX) focus not only on content side but also on easy styling.

build.gradle
dependencies {
    ...
    ..
    .
    compile 'builders.dsl:spreadsheet-builder-poi:1.0.5'
}

2.3 Excel Creation

Create a interface to encapsulate Excel generation:

src/main/java/example/micronaut/BookExcelService.java
package example.micronaut;

import java.util.List;
import io.micronaut.http.server.types.files.AttachedFile;

public interface BookExcelService {
    static final String SHEET_NAME = "Books";
    static final String HEADER_ISBN = "Isbn";
    static final String HEADER_NAME = "Name";
    static final String HEADER_EXCEL_FILE_SUFIX = ".xlsx";
    static final String HEADER_EXCEL_FILE_PREFIX = "books";
    static final String HEADER_EXCEL_FILENAME = HEADER_EXCEL_FILE_PREFIX + HEADER_EXCEL_FILE_SUFIX;

    AttachedFile excelFileFromBooks(List<Book> bookList); (1)
}
1 AttachedFile is used as the return value of a route execution to indicate the given file should be downloaded by the client instead of displayed.

Externalize your styles configuration into a class implementing builders.dsl.spreadsheet.builder.api.Stylesheet interface to maximize code reuse.

src/main/java/example/micronaut/BookExcelStylesheet.java
package example.micronaut;

import builders.dsl.spreadsheet.api.FontStyle;
import builders.dsl.spreadsheet.builder.api.CanDefineStyle;
import builders.dsl.spreadsheet.builder.api.Stylesheet;

public class BookExcelStylesheet implements Stylesheet {
    public static final String STYLE_HEADER = "header";

    @Override
    public void declareStyles(CanDefineStyle stylable) {
        stylable.style(STYLE_HEADER, st -> {
            st.font(f -> f.style(FontStyle.BOLD));
        });
    }
}

Create a bean which generates the excel file.

src/main/java/example/micronaut/BookExcelServiceImpl.java
package example.micronaut;

import builders.dsl.spreadsheet.builder.poi.PoiSpreadsheetBuilder;
import io.micronaut.http.server.types.files.AttachedFile;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.inject.Singleton;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.List;
import java.util.stream.Stream;

@Singleton (1)
public class BookExcelServiceImpl implements BookExcelService {

    private static final Logger LOG = LoggerFactory.getLogger(BookExcelServiceImpl.class);

    @Override
    public AttachedFile excelFileFromBooks(List<Book> bookList) {
        try {
            File file = File.createTempFile(HEADER_EXCEL_FILE_PREFIX, HEADER_EXCEL_FILE_SUFIX);
            PoiSpreadsheetBuilder.create(file).build(w -> {
                w.apply(BookExcelStylesheet.class);
                w.sheet(SHEET_NAME, s -> {
                    s.row(r -> Stream.of(HEADER_ISBN, HEADER_NAME)
                            .forEach(header -> r.cell(cd -> {
                                    cd.value(header);
                                    cd.style(BookExcelStylesheet.STYLE_HEADER);
                                })
                            ));
                    bookList.stream()
                            .forEach( book -> s.row(r -> {
                                r.cell(book.getIsbn());
                                r.cell(book.getName());
                            }));
                });
            });
            return new AttachedFile(file, HEADER_EXCEL_FILENAME);
        } catch (IOException e) {
            if (LOG.isErrorEnabled()) {
                LOG.error("File not found exception raised when generating excel file");
            }
        }
        return null;
    }
}
1 To register a Singleton in Micronaut’s application context annotate your class with javax.inject.Singleton

2.4 Controller

Add Server Side View Rendering and Thymeleaf dependencies:

build.gradle
dependencies {
    ...
    ..
    .
    compile "io.micronaut:views"
    runtime "org.thymeleaf:thymeleaf:3.0.9.RELEASE"
}

Create a controller:

src/main/java/example/micronaut/HomeController.java
package example.micronaut;

import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.micronaut.http.server.types.files.AttachedFile;
import io.micronaut.views.View;

import java.util.HashMap;
import java.util.Map;

@Controller("/") (1)
public class HomeController {

    protected final BookRepository bookRepository;
    protected final BookExcelService bookExcelService;

    public HomeController(BookRepository bookRepository,  (2)
                          BookExcelService bookExcelService) {
        this.bookRepository = bookRepository;
        this.bookExcelService = bookExcelService;
    }

    @View("index") (3)
    @Get
    Map<String, String> index() {
        return new HashMap<>();
    }

    @Get("/excel") (4)
    AttachedFile excel() { (5)
        return bookExcelService.excelFileFromBooks(bookRepository.findAll());
    }
}
1 The class is defined as a controller with the @Controller annotation mapped to the path /
2 Constructor injection
3 Use @View annotation to specify which template would you like to render the response against.
4 You can specify the HTTP verb for which a controller’s action responds to. To respond to a GET request, use io.micronaut.http.annotation.Get
5 AttachedFile is used as the return value of a route execution to indicate the given file should be downloaded by the client instead of displayed.

The previous controller index method renders a simple view with a link to download the excel file:

src/main/resources/views/index.html
<!DOCTYPE html>
<html>
<head>
    <title>Micronaut</title>
</head>
<body>
<p><a href="/excel">Excel</a></p>
</body>
</html>

2.5 Tests

Often, file transfers remain untested in many applications. In this section, you will see how easy is to test that the file downloads but also that the downloaded file contents match our expectations.

Micronaut is test framework agnostic. You can use JUnit, Spock Framework or Spek.

In this Guide, we test the app with Spock Framework.

We need to modify build.gradle.

Replace apply plugin: 'java' with apply plugin: 'groovy' and add the necessary dependencies:

build.gradle
dependencies {
...
..
    testCompile "org.codehaus.groovy:groovy-all:2.5.2"
    testCompile "org.spockframework:spock-core:1.1-groovy-2.4"
}

Edit micronaut-cli.yml to set Spock as the test framework:

micronaut-cli.yml
profile: service
defaultPackage: example.micronaut
---
testFramework: spock
sourceLanguage: java

We use also, Geb; a browser automation solution.

To use Geb, add dependencies:

build.gradle
    testCompile "org.gebish:geb-spock:2.2"
    testRuntime "org.seleniumhq.selenium:selenium-chrome-driver:3.12.0"
    testCompile "org.seleniumhq.selenium:selenium-support:3.12.0"
    testCompile "org.seleniumhq.selenium:selenium-remote-driver:3.12.0"
    testCompile "org.seleniumhq.selenium:selenium-api:3.12.0"

Create a Geb Configuration file. We configure some chrome options to control the download path.

src/test/resources/GebConfig.groovy
import org.openqa.selenium.chrome.ChromeDriver
import org.openqa.selenium.chrome.ChromeOptions

ChromeOptions options = new ChromeOptions()
if ( System.getProperty('download.folder') ) {
    options.setExperimentalOption("prefs", [
            "profile.default_content_settings.popups":  0, (1)
            "download.default_directory": System.getProperty('download.folder') (2)
    ])
}

environments {
    chrome {
        driver = { new ChromeDriver(options) }
    }
    chromeHeadless {
        driver = {
            options.addArguments('headless')
            new ChromeDriver(options)
        }
    }
}
1 Disable confirmation popups
2 Configure the download folder

Geb uses the Page concept pattern - The Page Object Pattern gives us a common sense way to model content in a reusable and maintainable way. Create a Geb Page to encapsulate the Excel link:

src/test/groovy/example/micronaut/HomePage.groovy
package example.micronaut

import geb.Page

class HomePage extends Page {

    static at = { title == 'Micronaut' }

    static url = '/'

    static content = {
        excelLink { $('a', text: 'Excel', 0) }
    }

    void downloadExcel() {
        excelLink.click()
    }
}

Install webdriver-binaries Gradle plugin; a plugin that downloads and caches WebDriver binaries specific to the OS the build runs on.

build.gradle
buildscript {
    repositories {
...
..
    }
    dependencies {
        classpath "gradle.plugin.com.energizedwork.webdriver-binaries:webdriver-binaries-gradle-plugin:1.4"
    }
}

apply plugin:"com.energizedwork.webdriver-binaries"


dependencies {
...
..
.
}

webdriverBinaries {
    chromedriver '2.41'
    geckodriver '0.21.0'
}

tasks.withType(Test) {
    systemProperty "geb.env", System.getProperty('geb.env') (1)
    systemProperty "download.folder", System.getProperty('download.folder') (2)
    beforeTest { descriptor -> logger.quiet " -- $descriptor" }
    testLogging {
        events "passed", "skipped", "failed"
        exceptionFormat 'full'
    }
}
1 Pass system property geb.env to the tests.
2 Pass system property download.folder to the tests.

Create a test which verifies the Excel file is downloaded and the content matches our expectations.

src/test/groovy/example/micronaut/DownloadExcelSpec.groovy
package example.micronaut

import builders.dsl.spreadsheet.query.api.SpreadsheetCriteria
import builders.dsl.spreadsheet.query.api.SpreadsheetCriteriaResult
import builders.dsl.spreadsheet.query.poi.PoiSpreadsheetCriteria
import geb.spock.GebSpec
import io.micronaut.context.ApplicationContext
import io.micronaut.context.env.Environment
import io.micronaut.runtime.server.EmbeddedServer
import spock.lang.AutoCleanup
import spock.lang.IgnoreIf
import spock.lang.Shared
import spock.util.concurrent.PollingConditions

class DownloadExcelSpec extends GebSpec {

    @Shared
    @AutoCleanup (1)
    EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer, [:], Environment.TEST) (2)

    @IgnoreIf({ !sys['download.folder'] || sys['geb.env'] != 'chrome' })
    def "books can be downloaded as an excel file"() {
        given:
        PollingConditions conditions = new PollingConditions(timeout: 5)
        browser.baseUrl = "http://localhost:${embeddedServer.port}" (3)

        when:
        browser.to HomePage

        then:
        browser.at HomePage

        when: 'clicking excel button'
        String expectedPath = System.getProperty('download.folder') + "/" + BookExcelService.HEADER_EXCEL_FILENAME
        File outputFile = new File(expectedPath)
        browser.page(HomePage).downloadExcel()

        then: 'an excel file is downloaded'
        conditions.eventually { outputFile.exists() }

        when: 'if we search for a row with a particular value (Building Microservices)'
        SpreadsheetCriteria query = PoiSpreadsheetCriteria.FACTORY.forFile(outputFile)
        SpreadsheetCriteriaResult result = query.query {
            sheet(BookExcelService.SHEET_NAME) {
                row {
                    cell {
                        value 'Building Microservices'
                    }
                }
            }
        }

        then: 'a row is found'
        result.cells.size() == 1

        cleanup:
        outputFile?.delete()
    }
}
1 The AutoCleanup extension makes sure the close() method of an object (e.g. EmbeddedServer) is called each time a feature method is finished
2 To run the application from a unit test you can use the EmbeddedServer interface
3 The EmbeddedServer interface provides the URL of the server under test which runs on a random port. We use this port to set Geb base url.

To run the tests:

$ ./gradlew -Dgeb.env=chrome -Ddownload.folder=/Users/sdelamo/Downloads test
$ open build/reports/tests/test/index.html

3 Running the app

To run the application use the ./gradlew run command which will start the application on a random port.

4 Next Steps

Read more about File Transfers support inside Micronaut.