ngs-doo / TemplaterExamples

Creating reports in .NET and Java
https://templater.info
The Unlicense
48 stars 27 forks source link

Leave Tag Unprocessed if Value is Null #64

Closed damianhofmannwork closed 1 year ago

damianhofmannwork commented 1 year ago

I am processing reports, which must be "complete". Complete means, that all tags must be replaced with a valid value. I achieve this by reading the remaining tags raising an exception if the template still contains tags after processing.

During testing, I have noticed that Templater replaces (collapses) tags with empty String, if the property in the input object exists, but has value null.

I failed to find examples or documentation on how to change this default behavior. I want to configure Templater such that, if a property has value null (which means "missing value" in my context), it does not process the tag. I still want to use [[thisTagMayBeNull]:empty(Value Missing)] in places, where I expect null values. But I want the default to leave the tag alone, if there's no value.

Can you provide (or point me to) a Java example or the relevant documentation on how to achieve this? Thank you very much.

zapov commented 1 year ago

There are several approaches to this. One would be to implement lowlevel replacer plugin and inspect the provided value for null. Then track this tag and at the end of processing you should know that there was such cases. You can use something like thread local or configuration setup per processing as this plugins are shared during processing.

You can find low level replace and page 31 of user manual. There is also an example here: https://github.com/ngs-doo/TemplaterExamples/blob/4bb7cce55240c4aa50b19504e867b3a17e59500c/Beginner/MailMerge/src/main/java/hr/ngs/templater/example/MailMergeExample.java#L38

zapov commented 1 year ago

Also, its not clear if thats your case, but Templater has some special logic when it detects unknown tags which "should" be processed. You can control that via UnprocessedTags plugin, which is described on page 34. There is also example here: https://github.com/ngs-doo/TemplaterExamples/blob/6709e6c4e6f65ec09b3c2540e0ea22d4bd19aad0/Intermediate/MissingProperty/src/Program.cs#L25

zapov commented 1 year ago

Also, one more "trick" you can do is instead of replacing it with null in low level replacer, you can replace it with the actual tag value. This way it will look as the tag was not replaced at all, but it should be processed. Also, you can register your own empty handler which will behave differently. Keep in mind that unless you disable built-in handlers, default one will still run. Here is an override for built-in bool handler: https://github.com/ngs-doo/TemplaterExamples/blob/master/Intermediate/BoolOverride/src/main/java/hr/ngs/templater/example/BoolExample.java#L14

Do let me know if you manage to resolve it or get stuck.

damianhofmannwork commented 1 year ago

Thanks @zapov

I think the first approach is "less invasive". Disabling the built-in handlers (as suggested in your third post) might have side-effects, which I don't fully understand.

Here's what I ended up with, according to your first suggestion:

public class NullValueTracker implements DocumentFactoryBuilder.LowLevelReplacer {
    private final Set<String> tagsWithNullValues = new LinkedHashSet<>();

    @Override
    public Object replace(Object value, String tag, String[] metadata) {
        if (value == null) {
            tagsWithNullValues.add(tag);
        }
        return value;
    }

    public Set<String> getTagsWithNullValues() {
        return tagsWithNullValues;
    }
}

Then I use it like this:

File templateFile = new File("path/to/my/template.docx");
File outputFile = new File("path/to/my/output.docx");

NullValueTracker nullValueTracker = new NullValueTracker();
DocumentFactory documentFactory = Configuration.builder().include(nullValueTracker).build();
try (InputStream templateStream = Files.newInputStream(templateFile.toPath());
     OutputStream outputStream = Files.newOutputStream(outputFile.toPath());
     TemplateDocument template = documentFactory.open(templateStream, "docx", outputStream)) {

    template.process(businessObject);

    // collect unprocessed and empty tags
    final var unprocessedTags = new LinkedHashSet<String>();
    unprocessedTags.addAll(nullValueTracker.getTagsWithNullValues());
    unprocessedTags.addAll(asList(template.templater().tags()));
    if (!unprocessedTags.isEmpty()) {
        throw new RuntimeException("Incomplete report!");
    }
} catch (Exception e) {
    throw new RuntimeException("Failed to generate report!", e);
}

This seems to work quite well, but I'm still testing.