I like to use programs that can remember what I was doing the last time I was working with them. They should restore the window just as I had it, remember which file(s) I was working with, what preferences I had selected, and so on. Naturally, I want the programs I write to be just as considerate of the user.
For some time, I’ve been fretting over the best way to do this in a Clojure program. Should I provide wrappers around the Java Preferences API? Some other mechanism? Turns out I should just embrace simplicity.
Simple configuration data usually consists of key/value pairs. In Clojure, the natural data structure to use is the map. For example, the size and position of a program window might be encoded as something like this:
The structure of the values can actually be arbitrarily complex. They can hold just about anything.
It turns out that Clojure has facilities built in that can store and retrieve such information from disk files easily. The following is a demonstration of how saving and restoring such information can be done. First, here’s a project file:
Nothing tricky here except making sure that the
:resource-paths point to the location of the JavaFX runtime jar.
The progam itself:
The demo works by linking into the program start-up and shutdown processes. Since this is a JavaFX program, the hooks can be inserted with the
.setOnCloseRequest methods of the stage object. The
handle-close-request functions return
EventHandlers for these two notifications respectively.
The operation of the program is straightforward. On closing the program, the relevant configuration information is updated by the
update-config function and written to a file in the project directory,
config.clj, by the
When the program starts, the
config.clj file is read by the
read-config function and the configuration information is restored before the program window is displayed. There are two things to note about the
read-config function. First, on the very first execution of the program, there is no configuration file on disk. In that case the program throws and catches an exception. When the exception is caught, the
default-config function is called to create a reasonable group of default settings that the program should use.
Note that the program can still crash if the configuration file is present but contains bogus values, like an x position of “bogus”, example.
The second interesting thing about read-config is that it uses
clojure.edn/read-string instead of
clojure.core/read-string. Since in Clojure, like all Lisps, program code is just data, and data can be program code, the use of the
clojure.core/read-string could allow execution of arbitrary code from a malicious configuration file. This is true even if the Clojure global variable
*read-eval* is set to false, since earlier versions of Clojure could still be tricked into running Java constructors through this path. See the discussion of
Granted, it is pretty unlikely that a user would sabotage their own machine this way for this use case, but why not get into the habit of using
clojure.edn/read-string anyway? It is a drop-in replacement for
clojure.core/readstring for this purpose.
If you would like to try this yourself, you can download a project repository from here.