Skip to main content

Implementing a Custom Reporter

While Liquibase Linter has some good core reporters, you may have a use case that's particular to your project and wouldn't make sense as a core reporter.

Fortunately it's trivial to implement custom reporters and apply them in your own project; you just need to write a Java class implementing the reporting interface and do a little configuration.

Writing the reporter

There are a few interfaces to implement when writing a custom reporter in Java:

  • Reporter is the actual interface that your reporter must implement
  • Reporter.Factory is the piece which ties the reporter into the lqlint.json configuration.

Fortunately, there are existing abstract classes that make this easy to do. Furthermore, the Reporter.Factory can exist as inner classes to the main Reporter implementation.

Sample Reporter implementation

package com.fake.fancyapp.liquibase;

import java.io.PrintWriter;
import java.util.List;
import report.io.github.liquibaselinter.AbstractReporter;
import report.io.github.liquibaselinter.Report;
import report.io.github.liquibaselinter.ReportItem;
import report.io.github.liquibaselinter.ReporterConfig;

public class CustomReporter extends AbstractReporter {

private static final String NAME = "custom-reporter";

public CustomReporter(ReporterConfig config) {
super(config, "ext"); // reports will have a `.ext` file extension
}

@Override
protected void printReport(PrintWriter output, Report report, List<ReportItem> items) {
// The 'items' have already been filtered.
// All that is left to do is produce the output.
// Alternatively, extend an existing core reporter and override methods.
}

public static class Factory extends AbstractReporter.Factory<CustomReporter> {

public Factory() {
super(NAME);
}
}
}

Some notes about how we've done this:

  • Extend the AbstractReporter class, which saves us from creating a lot of boilerplate ourselves.
  • Create Factory. This links the CustomReporter to the Liquibase Linter configuration.

All the core reporters are implemented in this way as well, so if you're not sure how best to hook something up you might try looking in the source at some existing core reporters that do something similar

Making the reporter discoverable

The class above should go into a new Maven project that depends on both liquibase-linter and liquibase.

The fact that the class exists isn't quite enough on its own; we need to tell Liquibase Linter that it's there. For this we are using the Service Provider Interface pattern - this is natively supported in Java and for use cases like this is preferable to powerful-but-heavy classpath scanning approaches like that used by Spring.

In our newly-created project, we'll create a new file at:

src/main/resources/META-INF/services/report.io.github.liquibaselinter.Reporter.Factory

And in the file, we'll write:

com.fake.fancyapp.liquibase.CustomReporter.Factory

Configuring the reporter in Maven

In the project where our scripts live, we'll add a dependency on our reporters project to liquibase-maven-plugin, in much the same way that we added a dependency for liquibase-linter originally. So for our example custom reporting project wcg-liquibase-linter we would have the following dependency.

<plugin>
<groupId>org.liquibase</groupId>
<artifactId>liquibase-maven-plugin</artifactId>
<configuration>...</configuration>
<dependencies>
<dependency>
<groupId>io.github.liquibase-linter</groupId>
<artifactId>liquibase-parser-extension</artifactId>
</dependency>
<dependency>
<groupId>com.fake.fancyapp</groupId>
<artifactId>liquibase-reporters</artifactId>
<version>0.1.0</version>
</dependency>
</dependencies>
<executions>...</executions>
</plugin>

Then all we need is to configure the reporter as normal in lqlint.json.

Adding custom config to the Reporter

If additional configuration opens are required for the CustomReporter to operate, extend ReporterConfig and create a new builder that extends ReporterConfig.BaseBuilder and change the Factory to extend ReporterConfig.BaseFactory, adding in the Config class to the generic type declaration. Add @JsonDeserialize to the Config class so that the CustomReporter.Config can be loaded from the Liquibase Linter configuration.

package com.fake.fancyapp.liquibase;

import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import java.io.PrintWriter;
import java.util.List;
import report.io.github.liquibaselinter.AbstractReporter;
import report.io.github.liquibaselinter.Report;
import report.io.github.liquibaselinter.ReportItem;
import report.io.github.liquibaselinter.ReporterConfig;

public class CustomReporter extends AbstractReporter {

private static final String NAME = "custom-reporter";

final String customConfigOption;

protected CustomReporter(CustomReporter.Config config) {
super(config);
this.customConfigOption = config.customConfigOption;
}

@Override
protected void printReport(PrintWriter output, Report report, List<ReportItem> items) {
// The 'items' have already been filtered.
// All that is left to do is produce the output.
// Alternatively, extend an existing core reporter and override methods.
}

public static class Factory extends AbstractReporter.BaseFactory<CustomReporter, Config> {

public Factory() {
super(NAME);
}
}

@JsonDeserialize(builder = Builder.class)
public static class Config extends ReporterConfig {

final String customConfigOption;

public Config(Builder builder) {
super(builder);
customConfigOption = builder.customConfigOption;
}
}

public static class Builder extends ReporterConfig.BaseBuilder<Builder> {

String customConfigOption;

public Builder withCustomConfigOption(String customConfigOption) {
this.customConfigOption = customConfigOption;
return this;
}

@Override
public Config build() {
return new Config(this);
}
}
}