Here's your servant, Sir. Out o' the process.
Weller is like Alfresco out-of-process extensions but in Clojure.
Filters are used for selecting messages that will flow through a tap. Filters can discriminate a message by matching several conditions (e.g. a node that has been created or deleted, an aspect that has been added to a node, a property that changed its value, etc.).
Filters can be made of a single predicate:
(pred/event? events/node-updated)
or by a composition of predicates using every-pred (logical AND) or some-fn (logical OR) Clojure built-in functions.
E.g. this matches every updated node which is also a file:
(every-pred (pred/event? events/node-updated)
(pred/is-file?))
this one matches every node with cm:titled or cm:dublincore aspects (regardless of the event type):
(some-fn (pred/node-aspect? cm/asp-titled)
(pred/node-aspect? cm/asp-dublincore))
and this one matches updated nodes with cm:titled or cm:dublincore aspects:
(every-pred (pred/event? events/node-updated)
(some-fn (pred/node-aspect? cm/asp-titled)
(pred/node-aspect? cm/asp-dublincore)))
The built-in predicates are available here while the events here.
Functions are the pieces of code deputed to take the message and do something with it. The resource is a map representing a node in Alfresco.
A function that prints the node name could be as simple as:
(defn print-node-name
[resource]
(println (:name resource)))
a more useful function that makes use of the CRAL library to update the node on Alfresco:
(defn add-aspect
[resource]
(let [aspect-names (get-in (nodes/get-node (c/ticket) (:id resource)) [:body :entry :aspect-names])]
(->> (model/map->UpdateNodeBody {:aspect-names (conj aspect-names cm/asp-dublincore)})
(nodes/update-node (c/ticket) (:id resource)))))
Note that the (resource part of) message is automatically converted to a plain Clojure map with keys changed to kebab-case and keywordized thus looks like this (representing a node in this case):
{:primary-assoc-qname "cm:49d3a98c-6a2d-4851-a3a5-6de719033b90",
:properties
{:cm:auto-version true,
:cm:version-type "MAJOR",
:cm:auto-version-on-update-props false,
:cm:version-label "1.0",
:cm:initial-version true},
:secondary-parents [],
:is-file true,
:is-folder false,
:created-by-user {:id "admin", :display-name "Administrator"},
:content
{:mime-type "application/octet-stream",
:size-in-bytes 0,
:encoding "UTF-8"},
:primary-hierarchy
["71290ab7-c07d-424e-a8b4-2eb3aefdd434"
"7cf72ac7-addc-4fe5-8af2-26d50dc9d575"
"81482fa8-3079-4187-96ae-43e36353d2c1"],
:name "49d3a98c-6a2d-4851-a3a5-6de719033b90",
:modified-at "2024-05-17T12:25:06.629Z",
:node-type "cm:content",
:id "8b799b25-6e1e-4ec9-92ad-7db2d6e5598e",
:modified-by-user {:id "admin", :display-name "Administrator"},
:aspect-names ["cm:versionable" "cm:auditable"],
:@type "NodeResource",
:created-at "2024-05-17T12:25:06.567Z"}
Configuration is stored internally in the atom weller.config/config. It contains a map like this:
{:alfresco {:scheme "http"
:host "localhost"
:port 8080
:core-path "alfresco/api/-default-/public/alfresco/versions/1"
:search-path "alfresco/api/-default-/public/search/versions/1"
:auth-path "alfresco/api/-default-/public/authentication/versions/1"
:discovery-path "alfresco/api/discovery"
:user "admin"
:password "admin"}
:activemq {:scheme "tcp"
:host "localhost"
:port 61616
:topic "alfresco.repo.event2"}}
When a message pipe is started then weller.config/configure is called and will try to load these files (in order):
(def ^:private cfg-files
["resources/weller.edn"
"~/.weller/weller.edn"
"./weller.edn"])
each (map contained in) file is (deep) merged with the previous, and values being overwritten by the most recent. Placeholders for environment variables can be used, e.g.:
{:alfresco {:user "${ALF_USER:admin}"
:password "${ALF_PASS:admin}"}}
will look for ALF_USER and ALF_PASS environment variables if defined, otherwise the default value will be used. Environment variables will win over the configuration files.
Lastly cral.config/configure is called and an Alfresco ticket is requested and then stored in :alfresco :ticket
section of the map. It can be retrieved with (c/ticket) (assuming that config has been imported as
[weller.config :as c]).
The standard constructor make-pipe will create a pipe that receives ActiveMQ messages. Then at least one tap must be
connected using the function add-filtered-tap that takes a filter and a (processing) function as arguments (note that
in Clojure functions are first-class and can be passed-to or returned-from other functions). Finally the pipe can be
started manually by calling component/start on it.
(-> (pipe/make-pipe)
(pipe/add-filtered-tap (pred/event? events/node-created) process-created-node)
(pipe/add-filtered-tap (pred/event? events/node-deleted) process-deleted-node)
(component/start))
You can also call add-filtered-tap on a running pipe: the new handler starts
immediately. Keep the returned pipe value so subsequent stop and removal
operations include the new tap.
The quick constructor takes directly a filter and a function. A tap will be created and connected internally and the pipe started automatically:
(pipe/make-pipe (pred/event? events/node-created) process-created-node)
Malformed JSON, JSON values other than objects, and JMS messages other than
TextMessage are logged and discarded. If a processing function throws an
exception, the handler logs the error and continues with the next message.
Failed messages are not retried automatically; callbacks may have already
performed side effects before failing. JMS uses AUTO_ACKNOWLEDGE, so callback
failure does not trigger broker redelivery.
JMS reception and each message handler run on blocking workers, separate from
the core.async go dispatch pool. Each handler processes messages sequentially.
Stopping a handler lets its current callback finish before the worker exits.
Requires Java 25 and the Clojure CLI. Dependencies are declared in deps.edn;
artifact coordinates and version are defined in build.clj.
clojure -M:repl # REPL in weller.core, including test namespaces
clojure -M:dev # REPL with source and test paths
clojure -M:test # integration tests (Alfresco and ActiveMQ required)
clojure -T:build jar # library source JAR and Maven POM
clojure -T:build uber # AOT-compiled standalone JAR
clojure -T:build install # build and install into the local Maven repository
clojure -T:build clean # remove target/
clojure -T:build info # print artifact coordinates and version
The integration tests use the Alfresco and ActiveMQ services started with
./alfresco.sh start (or alfresco.bat start on Windows), configured through weller.edn.
They create, update and delete test content. CI builds the library and compiles
all source namespaces without requiring those services.
The standalone JAR preserves the previous uberjar packaging. weller.core
currently has no -main function; start message pipes from the REPL or from your
own application entry point.
To publish to Clojars, run ./deploy.sh and enter your username and deploy token
(hidden while typing). Alternatively, set CLOJARS_USERNAME and CLOJARS_PASSWORD
(a deploy token), then run clojure -T:build deploy.
For a local documentation preview, run ./cljdoc-preview.sh (Docker required)
and open http://localhost:9000.
Copyright (c) 2023-2026 Saidone
Distributed under the GNU General Public License v3.0
Can you improve this documentation? These fine people already did:
saidone, Marco Marini & SaidoneEdit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |