From 264655a8ba70b99e8f16ae8943131441044da872 Mon Sep 17 00:00:00 2001 From: Janos Erdos Date: Sun, 7 Oct 2018 17:52:25 +0200 Subject: [PATCH] release 0.2.3: added dockerized example application (#1) --- README.md | 6 +-- docs/index.md | 1 + out.docx | 0 project.clj | 2 +- service/.gitignore | 11 ++++ service/Dockerfile | 21 ++++++++ service/README.md | 75 ++++++++++++++++++++++++++++ service/build.sh | 3 ++ service/project.clj | 10 ++++ service/run.sh | 10 ++++ service/src/stencil/service/core.clj | 68 +++++++++++++++++++++++++ src/stencil/api.clj | 16 +++++- 12 files changed, 217 insertions(+), 6 deletions(-) create mode 100644 out.docx create mode 100644 service/.gitignore create mode 100644 service/Dockerfile create mode 100644 service/README.md create mode 100755 service/build.sh create mode 100644 service/project.clj create mode 100755 service/run.sh create mode 100644 service/src/stencil/service/core.clj diff --git a/README.md b/README.md index d031c51f..d3b1e00c 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ them to make the template more readable. ## Version -**Latest stable** version is `0.2.2`. +**Latest stable** version is `0.2.3`. If you are using Maven, add the followings to your `pom.xml`: @@ -30,7 +30,7 @@ The dependency: io.github.erdos stencil-core - 0.2.2 + 0.2.3 ``` @@ -45,7 +45,7 @@ And the [Clojars](https://clojars.org) repository: Alternatively, if you are using Leiningen, add the following to the `:dependencies` section of your `project.clj` -file: `[io.github.erdos/stencil-core "0.2.2"]` +file: `[io.github.erdos/stencil-core "0.2.3"]` Previous versions are available on the [Stencil Clojars](https://clojars.org/io.github.erdos/stencil-core) page. diff --git a/docs/index.md b/docs/index.md index cb4b8296..77a71049 100644 --- a/docs/index.md +++ b/docs/index.md @@ -17,6 +17,7 @@ You can find the project on [Stencil's GitHub](https://github.com/erdos/stencil) ## For Programmers - First of all, read the [Getting Started](GettingStarted.md) manual +- Try out the [Dockerized Stencil](https://github.com/erdos/stencil/blob/master/service/README.md) example application. - Source code is available on [Stencil's GitHub](https://github.com/erdos/stencil) - If you wish to contribute, read the [Contributing](Contribution.md) page diff --git a/out.docx b/out.docx new file mode 100644 index 00000000..e69de29b diff --git a/project.clj b/project.clj index 7c685667..d8e31a42 100644 --- a/project.clj +++ b/project.clj @@ -1,4 +1,4 @@ -(defproject io.github.erdos/stencil-core "0.2.2" +(defproject io.github.erdos/stencil-core "0.2.3" :url "https://github.com/erdos/stencil" :description "Templating engine for office documents." :license {:name "Eclipse Public License - v 2.0" diff --git a/service/.gitignore b/service/.gitignore new file mode 100644 index 00000000..c53038ec --- /dev/null +++ b/service/.gitignore @@ -0,0 +1,11 @@ +/target +/classes +/checkouts +pom.xml +pom.xml.asc +*.jar +*.class +/.lein-* +/.nrepl-port +.hgignore +.hg/ diff --git a/service/Dockerfile b/service/Dockerfile new file mode 100644 index 00000000..b9c55fa1 --- /dev/null +++ b/service/Dockerfile @@ -0,0 +1,21 @@ +FROM clojure AS build-env +WORKDIR /usr/src/myapp + +COPY project.clj /usr/src/myapp/ +RUN lein deps + +COPY . /usr/src/myapp + +RUN mv "$(lein uberjar | sed -n 's/^Created \(.*standalone\.jar\)/\1/p')" myapp-standalone.jar + +FROM openjdk:8-jre-alpine + +ENV STENCIL_HTTP_PORT 8080 +ENV STENCIL_TEMPLATE_DIR /templates + +VOLUME /templates + +WORKDIR /myapp +COPY --from=build-env /usr/src/myapp/myapp-standalone.jar /myapp/myapp.jar +ENTRYPOINT ["java", "-jar", "/myapp/myapp.jar"] +EXPOSE 8080 diff --git a/service/README.md b/service/README.md new file mode 100644 index 00000000..726bd68d --- /dev/null +++ b/service/README.md @@ -0,0 +1,75 @@ +# Dockerized service for Stencil templates + +This is a Dockerized example application running the Stencil template engine. +You can use + + +## Usage + +Building and running the container: + +1. Build the container with the `build.sh` command. +2. Run the container locally with the `run.sh` command. First parameter: directory of template files to mount as a volume. Second parameter: http port to listen on (defaults to `8080`). + +After you start the container, it will enumerate and print the names of +the template files it found in the volume. + +Rendering a template: + +``` +time curl -XPOST localhost:8080/test-control-loop.docx --header "Content-Type: application/json" --data '{"elems":[{"value": "first"}]}' > rendered.docx +``` + +Opening the output file shows that the file contents are rendered all right. + +``` +oowriter rendered.docx +``` + + +## API + +You can send requests over a HTTP api. + +**request:** + +- method: `POST` +- uri: relative path of template file in templates folder +- headers: `Content-Type: application/json` +- request body: a JSON map of template data. + +The different responses have different HTTP staus codes. + +**response (success)** + +- status: `200` +- headers: `Content-Type: application/octet-stream` +- content: rendered document ready to download. + +**response (template not found)** + +This happens then the given file name was not found in the template directory. + +- status: `404` +- content: plain text + +**response (template error)** + +This happens when the template file could not be prepared. For example: syntax +errors in template file, unexpected file format, etc. + +- status: `500` +- content: plain text describing error + +**response (eval error)** + +This happens when the template could not be evaluated. + +- status: `400` +- content: plain text describing error + +**response (wrong url)** + +This happens when you do not send a `POST` request. + +- status `405` diff --git a/service/build.sh b/service/build.sh new file mode 100755 index 00000000..4e1882ce --- /dev/null +++ b/service/build.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env sh + +docker build . --tag stencil-service:latest diff --git a/service/project.clj b/service/project.clj new file mode 100644 index 00000000..4272eba8 --- /dev/null +++ b/service/project.clj @@ -0,0 +1,10 @@ +(defproject io.github.erdos/stencil-service "0.2.3" + :description "Web service for the Stencil templating engine" + :url "https://github.com/erdos/stencil" + :license {:name "Eclipse Public License - v 2.0" + :url "https://www.eclipse.org/legal/epl-2.0/"} + :dependencies [[org.clojure/clojure "1.8.0"] + [io.github.erdos/stencil-core "0.2.3"] + [http-kit "2.2.0"] + [ring/ring-json "0.4.0"]] + :main stencil.service.core) diff --git a/service/run.sh b/service/run.sh new file mode 100755 index 00000000..1d8a298e --- /dev/null +++ b/service/run.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env sh + +# starts a stencil service docker container. +# - first parameter: directory of template files +# - second parameter: http port to listen on. + +STENCIL_HOST_TEMPLATE_DIR=${1:-/home/erdos/Joy/stencil/test-resources} +STENCIL_HOST_HTTP_PORT=${2:-8080} + +docker run -it -p $STENCIL_HOST_HTTP_PORT:8080 -v $STENCIL_HOST_TEMPLATE_DIR:/templates stencil-service:latest diff --git a/service/src/stencil/service/core.clj b/service/src/stencil/service/core.clj new file mode 100644 index 00000000..1d36ac1e --- /dev/null +++ b/service/src/stencil/service/core.clj @@ -0,0 +1,68 @@ +(ns stencil.service.core + (:gen-class) + (:import [java.io File]) + (:require [org.httpkit.server :refer [run-server]] + [stencil.api :as api] + [clojure.java.io :refer [file]] + [ring.middleware.json :refer [wrap-json-body]])) + +(set! *warn-on-reflection* true) + +(defn get-http-port [] + (Integer/parseInt (System/getenv "STENCIL_HTTP_PORT"))) + +(defn get-template-dir [] + (let [dir (file (System/getenv "STENCIL_TEMPLATE_DIR"))] + (if-not (.exists dir) + (throw (ex-info "Template directory does not exist!" {:status 500})) + dir))) + +(def -prepared (atom {})) + +(defn prepared [template-name] + (let [template-file (file template-name) + last-modified (.lastModified template-file)] + (or (get-in @-prepared [template-file last-modified]) + (let [p (api/prepare template-file)] + (swap! -prepared assoc template-file {last-modified p}) + p)))) + +(defn get-template [^String template-name] + (let [template-name (.substring (str template-name) 1) ;; so they dont start with / + parent (get-template-dir) + template (file parent template-name)] + (if (.exists template) + (prepared template) + (throw (ex-info "Template file does not exist!" {:status 404}))))) + +(defmacro wrap-err [& bodies] + `(try ~@bodies + (catch clojure.lang.ExceptionInfo e# + (if-let [status# (:status (ex-data e#))] + {:status status#, :body (.getMessage e#)} + (throw e#))))) + +(defn -app [request] + (wrap-err + (case (:request-method request) + :post + (if-let [prepared (get-template (:uri request))] + (let [rendered (api/render! prepared (:body request) :output :input-stream)] + {:status 200 + :body rendered + :headers {"content-type" "application/octet-stream"}})) + ;; otherwise: + (throw (ex-info "Method Not Allowed" {:status 405}))))) + +(def app (wrap-json-body -app {:keywords? false})) + +(defn -main [& args] + (let [http-port (get-http-port) + template-dir ^File (get-template-dir) + server (run-server app {:port http-port})] + (println "Started listening on" http-port "serving" (str template-dir)) + (println "Available template files: ") + (doseq [^File line (tree-seq #(.isDirectory ^File %) (comp next file-seq) template-dir) + :when (.isFile line)] + (println (str (.relativize (.toPath template-dir) (.toPath line))))) + (while true (read-line)))) diff --git a/src/stencil/api.clj b/src/stencil/api.clj index 5392eed1..5a3caca3 100644 --- a/src/stencil/api.clj +++ b/src/stencil/api.clj @@ -21,15 +21,27 @@ (defn render! "Takes a prepared template instance and renders it. - By default it returns an InputStream of the rendered document." + By default it returns an InputStream of the rendered document. + + Options map keys: + - {:output FNAME} renders output to file FNAME (string or File object). Throws exception + if file already exists and :overwrite? option is not set. + - {:output :input-stream} returns an input stream of the result document. + - {:output :reader} returns the input stream reader of the result document." [template template-data & {:as opts}] (let [template (prepare template) template-data (make-template-data template-data) result (API/render template ^TemplateData template-data)] (cond + (#{:stream :input-stream} (:output opts)) + (.getInputStream result) + + (#{:reader} (:output opts)) + (new java.io.InputStreamReader (.getInputStream result)) + (:output opts) (let [f (clojure.java.io/file (:output opts))] - (if (.exists f) + (if (and (.exists f) (not (:overwrite? opts))) (throw (ex-info "File already exists! " {:file f})) (do (.writeToFile result f) (str "Written to " f))))