diff --git a/docs/GettingStarted.md b/docs/GettingStarted.md index 70c5a9b3..2d88c7ed 100644 --- a/docs/GettingStarted.md +++ b/docs/GettingStarted.md @@ -12,6 +12,7 @@ The public Java API is accessible in the `io.github.erdos.stencil.API` class. 1. First, we have to prepare a template file. Call `API.prepare()` funtion with the template file. 2. Second, we can render the prepared template using the `API.render()` function. +3. When you do not use a prepared template instance any more, call its `cleanup()` method to free allocated resources. The following example takes a template from the file system, fills it with data from the arguments and writes the rendered document back to the file system. diff --git a/java-src/io/github/erdos/stencil/PreparedTemplate.java b/java-src/io/github/erdos/stencil/PreparedTemplate.java index 6b5ef82b..d1673f62 100644 --- a/java-src/io/github/erdos/stencil/PreparedTemplate.java +++ b/java-src/io/github/erdos/stencil/PreparedTemplate.java @@ -12,13 +12,6 @@ public interface PreparedTemplate { - /** - * Name of the original file. - * - * @return original template name - */ - String getName(); - /** * Original template file that was preprocessed. * @@ -56,4 +49,10 @@ default TemplateDocumentFormats getTemplateFormat() { * Set of template variables found in file. */ TemplateVariables getVariables(); + + /** + * Makes the templace clean up any resources allocated for it. Subsequential invocations of this method have no + * effects. Rendering the template after this method call will throw an IllegalStateException. + */ + void cleanup(); } \ No newline at end of file diff --git a/java-src/io/github/erdos/stencil/impl/CachingTemplateFactory.java b/java-src/io/github/erdos/stencil/impl/CachingTemplateFactory.java index 54ec7f75..28cc10ba 100644 --- a/java-src/io/github/erdos/stencil/impl/CachingTemplateFactory.java +++ b/java-src/io/github/erdos/stencil/impl/CachingTemplateFactory.java @@ -35,6 +35,8 @@ public PreparedTemplate prepareTemplateFile(File templateFile) throws IOExceptio if (cache.containsKey(templateFile)) { PreparedTemplate stored = cache.get(templateFile); if (stored.creationDateTime().toEpochSecond(ZoneOffset.UTC) <= templateFile.lastModified()) { + // TODO: this is so not thread safe. + stored.cleanup(); stored = templateFactory.prepareTemplateFile(templateFile); cache.put(templateFile, stored); } diff --git a/java-src/io/github/erdos/stencil/impl/ClojureHelper.java b/java-src/io/github/erdos/stencil/impl/ClojureHelper.java index c186637c..c2a98eee 100644 --- a/java-src/io/github/erdos/stencil/impl/ClojureHelper.java +++ b/java-src/io/github/erdos/stencil/impl/ClojureHelper.java @@ -12,34 +12,40 @@ public class ClojureHelper { /** - * Clojure :stream keyword + * Clojure :data keyword */ - public static final Keyword KV_STREAM = Keyword.intern("stream"); + public static final Keyword KV_DATA = Keyword.intern("data"); /** - * Clojure :variables keyword + * Clojure :format keyword */ - public static final Keyword KV_VARIABLES = Keyword.intern("variables"); + public static final Keyword KV_FORMAT = Keyword.intern("format"); /** - * Clojure :template keyword + * Clojure :function keyword */ - public static final Keyword KV_TEMPLATE = Keyword.intern("template"); + public static final Keyword KV_FUNCTION = Keyword.intern("function"); /** - * Clojure :data keyword + * Clojure :stream keyword */ - public static final Keyword KV_DATA = Keyword.intern("data"); + public static final Keyword KV_STREAM = Keyword.intern("stream"); /** - * Clojure :format keyword + * Clojure :template keyword */ - public static final Keyword KV_FORMAT = Keyword.intern("format"); + public static final Keyword KV_TEMPLATE = Keyword.intern("template"); /** - * Clojure :function keyword + * Clojure :variables keyword */ - public static final Keyword KV_FUNCTION = Keyword.intern("function"); + public static final Keyword KV_VARIABLES = Keyword.intern("variables"); + + /** + * Clojure :zip-dir keyword + */ + + public static final Keyword KV_ZIP_DIR = Keyword.intern("zip-dir"); // requires stencil.process namespace so stencil is loaded. static { diff --git a/java-src/io/github/erdos/stencil/impl/FileHelper.java b/java-src/io/github/erdos/stencil/impl/FileHelper.java index 6a20218e..c17a31b5 100644 --- a/java-src/io/github/erdos/stencil/impl/FileHelper.java +++ b/java-src/io/github/erdos/stencil/impl/FileHelper.java @@ -73,4 +73,36 @@ public static void forceMkdir(final File directory) throws IOException { } } } + + /** + * Recursively deletes a directory or a file. + * + * @param file to delete, not null + * @throws NullPointerException on null or invalid file + */ + @SuppressWarnings({"ResultOfMethodCallIgnored", "ConstantConditions"}) + public static void forceDelete(final File file) { + if (file.isDirectory()) { + for (File child : file.listFiles()) { + forceDelete(child); + } + } + file.delete(); + } + + /** + * Recursively marks a directory or a file for deletion on exit. + * + * @param file to delete, not null + * @throws NullPointerException on null or invalid file + */ + @SuppressWarnings({"ResultOfMethodCallIgnored", "ConstantConditions"}) + public static void forceDeleteOnExit(final File file) { + file.deleteOnExit(); + if (file.isDirectory()) { + for (File child : file.listFiles()) { + forceDeleteOnExit(child); + } + } + } } diff --git a/java-src/io/github/erdos/stencil/impl/NativeEvaluator.java b/java-src/io/github/erdos/stencil/impl/NativeEvaluator.java index 80e63bfa..601b9e42 100644 --- a/java-src/io/github/erdos/stencil/impl/NativeEvaluator.java +++ b/java-src/io/github/erdos/stencil/impl/NativeEvaluator.java @@ -48,7 +48,7 @@ public EvaluatedDocument render(PreparedTemplate template, TemplateData data) { } final Consumer> stopwatch = debugStopWatch(LOGGER); - stopwatch.accept(() -> "Starting document rendering for template " + template.getName()); + stopwatch.accept(() -> "Starting document rendering for template " + template.getTemplateFile()); final IFn fn = ClojureHelper.findFunction("do-eval-stream"); diff --git a/java-src/io/github/erdos/stencil/impl/NativeTemplateFactory.java b/java-src/io/github/erdos/stencil/impl/NativeTemplateFactory.java index f2fa4571..70f32851 100644 --- a/java-src/io/github/erdos/stencil/impl/NativeTemplateFactory.java +++ b/java-src/io/github/erdos/stencil/impl/NativeTemplateFactory.java @@ -14,8 +14,11 @@ import java.io.InputStream; import java.time.LocalDateTime; import java.util.*; +import java.util.concurrent.atomic.AtomicBoolean; import static io.github.erdos.stencil.TemplateDocumentFormats.ofExtension; +import static io.github.erdos.stencil.impl.ClojureHelper.KV_ZIP_DIR; +import static io.github.erdos.stencil.impl.FileHelper.forceDeleteOnExit; import static java.util.Collections.emptySet; import static java.util.Collections.unmodifiableSet; @@ -31,7 +34,7 @@ public PreparedTemplate prepareTemplateFile(final File inputTemplateFile) throws } try (InputStream input = new FileInputStream(inputTemplateFile)) { - return prepareTemplateImpl(templateDocFormat.get(), input); + return prepareTemplateImpl(templateDocFormat.get(), input, inputTemplateFile); } } @@ -46,7 +49,7 @@ private Set variableNames(Map prepared) { } @SuppressWarnings("unchecked") - private PreparedTemplate prepareTemplateImpl(TemplateDocumentFormats templateDocFormat, InputStream input) { + private PreparedTemplate prepareTemplateImpl(TemplateDocumentFormats templateDocFormat, InputStream input, File originalFile) { final IFn prepareFunction = ClojureHelper.findFunction("prepare-template"); final String format = templateDocFormat.name(); @@ -60,20 +63,20 @@ private PreparedTemplate prepareTemplateImpl(TemplateDocumentFormats templateDoc throw ParsingException.wrapping("Could not parse template file!", e); } - final String templateName = (String) prepared.get(Keyword.intern("template-name")); - final File templateFile = (File) prepared.get(Keyword.intern("template-file")); - final LocalDateTime now = LocalDateTime.now(); final TemplateVariables vars = TemplateVariables.fromPaths(variableNames(prepared)); + final File zipDirResource = (File) prepared.get(KV_ZIP_DIR); + if (zipDirResource != null) { + forceDeleteOnExit(zipDirResource); + } + return new PreparedTemplate() { - @Override - public String getName() { - return templateName; - } + final LocalDateTime now = LocalDateTime.now(); + final AtomicBoolean valid = new AtomicBoolean(true); @Override public File getTemplateFile() { - return templateFile; + return originalFile; } @Override @@ -88,13 +91,33 @@ public LocalDateTime creationDateTime() { @Override public Object getSecretObject() { - return prepared; + if (!valid.get()) { + throw new IllegalStateException("Can not render destroyed template!"); + } else { + return prepared; + } } @Override public TemplateVariables getVariables() { return vars; } + + @Override + public void cleanup() { + if (valid.compareAndSet(false, true)) { + // deletes unused temporary zip directory + if (zipDirResource != null) { + FileHelper.forceDelete(zipDirResource); + } + } + } + + // in case we forgot to call it by hand. + @Override + public void finalize() { + cleanup(); + } }; } } \ No newline at end of file diff --git a/java-test/io/github/erdos/stencil/impl/DirWatcherTemplateFactoryTest.java b/java-test/io/github/erdos/stencil/impl/DirWatcherTemplateFactoryTest.java index 1ab4a932..8aa8b086 100644 --- a/java-test/io/github/erdos/stencil/impl/DirWatcherTemplateFactoryTest.java +++ b/java-test/io/github/erdos/stencil/impl/DirWatcherTemplateFactoryTest.java @@ -86,11 +86,6 @@ public PreparedTemplate prepareTemplateFile(File templateFile) { calledFiles.add(templateFile); return new PreparedTemplate() { - @Override - public String getName() { - return templateFile.getName(); - } - @Override public File getTemplateFile() { return templateFile; @@ -110,6 +105,12 @@ public Object getSecretObject() { public TemplateVariables getVariables() { return null; } + + @Override + public void cleanup() { + // NOP + + } }; } }