From d52148a28b44a15e413539ac45a2dd32d9b19154 Mon Sep 17 00:00:00 2001 From: Janos Erdos Date: Mon, 22 Oct 2018 18:21:58 +0200 Subject: [PATCH] make PreparedTemplate instances clean up when discarded (#12) Originally a PreparedTemplate instance needs a temporary folder in the file system to hold its contents unzipped. Now the instances can clean up these directories when discarded either by calling cleanup() or finalize(). Also, these directories are marked with deleteOnExit. --- docs/GettingStarted.md | 1 + .../erdos/stencil/PreparedTemplate.java | 13 +++--- .../stencil/impl/CachingTemplateFactory.java | 2 + .../erdos/stencil/impl/ClojureHelper.java | 30 ++++++++----- .../github/erdos/stencil/impl/FileHelper.java | 32 +++++++++++++ .../erdos/stencil/impl/NativeEvaluator.java | 2 +- .../stencil/impl/NativeTemplateFactory.java | 45 ++++++++++++++----- .../impl/DirWatcherTemplateFactoryTest.java | 11 ++--- 8 files changed, 100 insertions(+), 36 deletions(-) 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 + + } }; } }