Context

The most common licensing mechanism for iText DITO is by volume of produced PDF documents. In order to count the number of times you use the pdf-producer method of the iText DITO API, the standard procedure is that the API connects with iText's cloud-based counter service. It runs on Amazon, located in the US. Due to company policies or guidelines, you may be unable to permit this connection to iText's counter mechanism. In that case, you can still make use of the volume-based licensing option, but you'll have to implement your own local PDF counter.

Create your own local document counter

It is quite easy to create a custom counter that will process each PDF production and record events on disk.

In this example we will create a counter that caches and accumulates PDF production info, and write this info on disk each 30 seconds. Such counters should be used in web server applications that are constantly running and perform a lot of PDF productions during there run time. This counter should be thread-safe.

As the the result of counter execution we want to create files in specified folder for each day with name in form of events-2020-12-31.json , with the content in form of:

events-2020-12-31.json
[{
  "count" : 47,
  "event_type" : "dito-produce-page",
  "event_time" : "2020-12-31 08:07:15"
},
{
  "count" : 2,
  "event_type" : "dito-produce",
  "event_time" : "2020-12-31 08:07:15"
},
{
  "count" : 15,
  "event_type" : "dito-produce-page",
  "event_time" : "2020-12-31 08:07:45"
},
{
  "count" : 15,
  "event_type" : "dito-produce",
  "event_time" : "2020-12-31 08:07:45"
}]

Here is an example of such counter:

LocalCounter.java
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectWriter;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import static java.time.temporal.ChronoField.HOUR_OF_DAY;
import static java.time.temporal.ChronoField.MINUTE_OF_HOUR;
import static java.time.temporal.ChronoField.SECOND_OF_MINUTE;

public class LocalCounter {

    private final static String DOCUMENT_EVENT_TYPE = "dito-produce";
    private final static String PAGE_EVENT_TYPE = "dito-produce-page";
    private final static long FLUSH_INTERVAL_MILLIS = TimeUnit.SECONDS.toMillis(30);
    private final static File OUTPUT_FOLDER = new File("./target/reports");

    private final static byte[] NEW_LINE_AS_BYTES = System.lineSeparator().getBytes(StandardCharsets.UTF_8);
    private final static char JSON_END_ARRAY = ']';
    private final static char JSON_COMMA = ',';

    private final static String FILE_PREFIX = "events-";
    private final static ZoneId UTC = ZoneId.of("UTC");
    private final static DateTimeFormatter FILE_DATE_FORMATTER =
            DateTimeFormatter.ISO_LOCAL_DATE.withZone(UTC);
    private final static DateTimeFormatter EVENT_TIME_DATE_FORMATTER = new DateTimeFormatterBuilder()
            .parseCaseInsensitive()
            .append(DateTimeFormatter.ISO_LOCAL_DATE)
            .appendLiteral(' ')
            .appendValue(HOUR_OF_DAY, 2)
            .appendLiteral(':')
            .appendValue(MINUTE_OF_HOUR, 2)
            .appendLiteral(':')
            .appendValue(SECOND_OF_MINUTE, 2)
            .toFormatter(Locale.ROOT).withZone(UTC);

    private final static Object CACHE_LOCK = new Object();
    private final static Object IO_LOCK = new Object();

    private final static AtomicReference<LocalCounter> counter = new AtomicReference<>(null);

    private final File folder;
    private final Map<String, Event> cache = new HashMap<>();
    private final Thread flushThread;
    private final AtomicBoolean threadStarted = new AtomicBoolean(false);

    private LocalCounter(File folder) {
        this.folder = folder;
        this.flushThread = new Thread(() -> {
            while (true) {
                try {
                    Thread.sleep(FLUSH_INTERVAL_MILLIS);
                    flush();
                } catch (InterruptedException ignored) {
                    break;
                }
            }
        });
    }

    /**
     * Starts the counter. Does nothing if the counter is already started.
     */
    public static void start() {
        LocalCounter local = counter.get();
        if (local == null) {
            LocalCounter newCounter = new LocalCounter(OUTPUT_FOLDER);
            while (local == null) {
                counter.compareAndSet(null, newCounter);
                local = counter.get();
            }
            local.startFlushThread();
        }
    }

    /**
     * Stops the counter. Does nothing if the counter is already stopped.
     */
    public static void stop() {
        LocalCounter local = counter.getAndSet(null);
        if (local != null) {
            local.interruptFlushThread();
        }
    }

    /**
     * Process PDF production report. Does nothing if the counter is stopped.
     */
    public static void process(PdfProductionResult report) {
        LocalCounter local = counter.get();
        if (local != null) {
            local.cache(report);
        }
    }

    private void cache(PdfProductionResult report) {
        cache(DOCUMENT_EVENT_TYPE, 1);
        cache(PAGE_EVENT_TYPE, report.getPageCount());
    }

    private void cache(String eventType, long eventCount) {
        Event newEvent = new Event().setType(eventType).setCount(eventCount)
                .setTime(EVENT_TIME_DATE_FORMATTER.format(Instant.now()));
        synchronized (CACHE_LOCK) {
            Event oldEvent = cache.put(eventType, newEvent);
            if (oldEvent != null) {
                newEvent.setCount(oldEvent.getCount() + newEvent.getCount());
            }
        }
    }

    private void flush() {
        File outputFile = getOutputFile();
        ObjectWriter jsonWriter = new ObjectMapper().writerWithDefaultPrettyPrinter();
        List<Event> cachedEvents = null;
        synchronized (CACHE_LOCK) {
            if (!cache.values().isEmpty()) {
                cachedEvents = new ArrayList<>(cache.values());
                cache.clear();
            }
        }
        if (cachedEvents != null && !cachedEvents.isEmpty()) {
            synchronized (IO_LOCK) {
                try (RandomAccessFile output = new RandomAccessFile(outputFile, "rw")) {
                    byte[] buffer = new byte[10];
                    long start = Math.max(output.length() - buffer.length, 0);
                    output.seek(start);
                    long bytesRead = output.read(buffer);
                    for (int i = 0; i < bytesRead; ++i) {
                        if (buffer[i] == JSON_END_ARRAY) {
                            output.seek(start + i);
                            output.write(JSON_COMMA);
                            output.write(NEW_LINE_AS_BYTES);
                        }
                    }
                    for (int i = 0; i < cachedEvents.size() - 1; ++i) {
                        jsonWriter.writeValue(output, cachedEvents.get(i));
                        output.write(JSON_COMMA);
                        output.write(NEW_LINE_AS_BYTES);
                    }
                    jsonWriter.writeValue(output, cachedEvents.get(cachedEvents.size() - 1));
                    output.write(JSON_END_ARRAY);
                    output.write(NEW_LINE_AS_BYTES);
                } catch (IOException exception) {
                }
            }
        }
    }

    private void startFlushThread() {
        if (!threadStarted.getAndSet(true)) {
            flushThread.start();
        }
    }

    private void interruptFlushThread() {
        flushThread.interrupt();
    }

    private File getOutputFile() {
        return new File(folder, FILE_PREFIX + FILE_DATE_FORMATTER.format(Instant.now()) + ".json");
    }
}

The Event  is a simple data class for events with some annotations declaring how it will be written to JSON file

Event.java
import com.fasterxml.jackson.annotation.JsonProperty;

public class Event {
    @JsonProperty("event_type")
    private String type;
    private long count;
    @JsonProperty("event_time")
    private String time;

    public String getType() {
        return type;
    }

    public Event setType(String type) {
        this.type = type;
        return this;
    }

    public long getCount() {
        return count;
    }

    public Event setCount(long count) {
        this.count = count;
        return this;
    }

    public String getTime() {
        return time;
    }

    public Event setTime(String time) {
        this.time = time;
        return this;
    }
}

The PdfProductionResult is class for reading REST API responses.

When writing a counter for an application that uses the native Java SDK instead of the REST API the com.itextpdf.dito.sdk.output.PdfProductionResult should be used instead.

PdfProductionResult.java
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;

@JsonIgnoreProperties(ignoreUnknown = true)
public class PdfProductionResult {
    private long pageCount;

    public long getPageCount() {
        return pageCount;
    }

    public PdfProductionResult setPageCount(long pageCount) {
        this.pageCount = pageCount;
        return this;
    }
}

Here is the example of how you can use it with iText DITO REST API running on local port 8080. This example uses PDF Production endpoint that returns PDFs as payload.

Example.java
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import org.apache.commons.io.IOUtils;
import org.apache.http.HttpHeaders;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.junit.Ignore;

public class Example {

    private static final String APPLICATION_PDF = "application/pdf";
    private static final String REQUEST_PAYLOAD = "{"
            + "\"templateProjectPath\":\"test.dito\","
            + "\"templateName\":\"output\","
            + "\"data\":{}"
            + "}";

    public static void main(String[] args) throws IOException {
		// Local counter should be started before processing
        LocalCounter.start();
		// ...
        ObjectMapper jsonMapper = new ObjectMapper();
        try (CloseableHttpClient client = HttpClients.createDefault()) {
            // Working with endpoint that produces PDF on disk. In this particular example iText DITO Docker SDK is running on local port 8080.
            HttpPost createPdfAsStreamRequest = new HttpPost("http://localhost:8080/api/pdf-producer");
            createPdfAsStreamRequest.setHeader(HttpHeaders.ACCEPT, APPLICATION_PDF);
            createPdfAsStreamRequest.setEntity(new StringEntity(REQUEST_PAYLOAD, ContentType.APPLICATION_JSON));
            String reportId;
            try (CloseableHttpResponse createPdfResponse = client.execute(createPdfAsStreamRequest);
                    InputStream pdfContent = createPdfResponse.getEntity().getContent();
                    OutputStream output = new FileOutputStream("target/produced.pdf")) {
                IOUtils.copy(pdfContent, output);
                // reads report id from header
                reportId = createPdfResponse.getFirstHeader("X-DITO-Producer-Report-ID").getValue();
            }
            // request report
            HttpGet getReport = new HttpGet("http://localhost:8080/api/pdf-producer/report/" + reportId);
            try (CloseableHttpResponse getReportResponse = client.execute(getReport);
                    InputStream reportContent = getReportResponse.getEntity().getContent()) {
                // read response json
                PdfProductionResult report = jsonMapper.readValue(reportContent, PdfProductionResult.class);
                LocalCounter.process(report);
                System.out.println(jsonMapper.writerWithDefaultPrettyPrinter().writeValueAsString(report));
            }
        }
        // ...
		// local counter can be stopped in the end of application
        LocalCounter.stop();
    }
}

Here is the example of how you can use it with iText DITO REST API running on local port 8080. This example uses the PDF Production endpoint that creates PDFs inside a Docker Container.

Example.java
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.io.InputStream;
import org.apache.http.HttpHeaders;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;

public class Example {

    private static final String APPLICATION_PDF = "application/pdf";
    private static final String REQUEST_PAYLOAD = "{"
            + "\"templateProjectPath\":\"test.dito\","
            + "\"templateName\":\"output\","
            + "\"data\":{}"
            + "}";

    public static void main(String[] args) throws IOException {
		// Local counter should be started before processing
        LocalCounter.start();
		// ...
        ObjectMapper jsonMapper = new ObjectMapper();
        try (CloseableHttpClient client = HttpClients.createDefault()) {
            // Working with endpoint that produces PDF on disk. In this particular example iText DITO Docker SDK is running on local port 8080.
            HttpPost createPdfRequest = new HttpPost("http://localhost:8080/api/pdf-producer");
            createPdfRequest.setHeader(HttpHeaders.ACCEPT, ContentType.APPLICATION_JSON.getMimeType());
            createPdfRequest.setEntity(new StringEntity(REQUEST_PAYLOAD, ContentType.APPLICATION_JSON));
            try (CloseableHttpResponse createPdfResponse = client.execute(createPdfRequest);
                    InputStream content = createPdfResponse.getEntity().getContent()) {
                // reads response json and gets report
                PdfProductionResult report = jsonMapper
                        .readValue(content, PdfProducerResponseDescriptor.class)
                        .getReport();
                LocalCounter.process(report);
            }
        }
        // ...
		// local counter can be stopped in the end of application
        LocalCounter.stop();
    }
}

The PdfProducerResponseDescriptor  is a data class for reading REST API response.

PdfProducerResponseDescriptor.java
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;

@JsonIgnoreProperties(ignoreUnknown = true)
public class PdfProducerResponseDescriptor {
    private PdfProductionResult report;

    public PdfProductionResult getReport() {
        return report;
    }

    public PdfProducerResponseDescriptor setReport(PdfProductionResult report) {
        this.report = report;
        return this;
    }
}