57

I am using Leiningen in my Clojure project (a GUI application) and created a "resources" directory under the project root to hold images that my app uses.

When I am running my app locally during testing, I fetch the images using the relative path "resources/logo.png", and this works fine. But when I build an uberjar using Leiningen, Leiningen puts the files from the resources folder in the JAR's root folder, so my references to resource files don't work anymore.

What is the correct way to access resources like this using Leiningen?

Kevin Albrecht
  • 6,974
  • 7
  • 44
  • 56

8 Answers8

52

The previous answerer (skuro) pointed out that I need to get the file from the classpath. After a little more digging, this appears to be the solution that works for my case:

(.getFile (clojure.java.io/resource "foo.png"))
Kevin Albrecht
  • 6,974
  • 7
  • 44
  • 56
  • 7
    `(.getPath)` is more accurate, as `(.getFile)` also appends possibly existing query parameters which should not exist in resource URLs. – Kreisquadratur Oct 29 '13 at 06:47
  • For me it still doesn't work. `(io/resource "file.png")` returns `nil` but `file.png` is included in the `jar` file. So I'm not sure what's happening. `.getFile` simply results in NPE so there is no way of using `slurp` on it. – vvwccgz4lh Sep 24 '20 at 07:24
  • Found my issue. I'm using ring to serve a directory from a `resources/.../` and it works during development. But after I build a `jar` the validation in the `ring.util.response/resource-response` fails. Therefore it returns `nil` because it tries to check canonical path and it doesn't match because it's in a `jar`. – vvwccgz4lh Sep 24 '20 at 10:06
34

Just a syntax sugar for the answer of Kevin Albrecht:

(require '[clojure.java.io :as io])

(-> "foo.png" io/resource io/file) 
Valerii Hiora
  • 1,542
  • 13
  • 8
  • 5
    Before anybody starts slurping on the file, be aware that once packaged in a jar this won't work. slurp can work on the resource itself: http://stackoverflow.com/questions/32232662/clojure-uberjar-not-loading-resource-file – cburgmer May 21 '16 at 13:20
22

This is not directly answering the OP's question, but skuro mentioned something in his last paragraph which can be quite useful, i.e. to make resources available on the classpath during development, but not include them in the release jar/uberjar.

The reason of that is then you could place the resources separately outside of the jar – for example placing configuration files under /etc – and then link to them at runtime by listing such resource folders on the classpath.

Definitions in project.clj

  • remove the top-level :resource-paths from your project.clj;
  • add the following lines in the project.clj at the top level:
:profiles {:dev {:resource-paths ["src/main/resources"]}}

This way the resources will be available on the classpath during development (compile, test, repl), but will not be included in the release jars.

As an altenative, you might want to bundle some of the resources with the release jar, like application icons and graphics, but not others, like configuration files. In that case you could split the resources folder into subdirectories and declare the ones you want to get included at the top level, while the rest under the dev profile:

:resource-paths ["src/main/resources/icons"
                 "src/main/resources/images"]
:profiles {:dev {:resource-paths ["src/main/resources/config"]}}

Referencing resources in code

Using the above method you could reference resources both during development and production uniformly by looking them on the classpath (as shown by Kevin and Valerii), and not directly on the filesystem:

(require '[clojure.java.io :as jio])
(jio/file (jio/resource "relative/path/to/resource.xml"))

Runtime classpath

During development Leiningen would include both the top level and the dev profile resources on the classpath. For the released runtime you will have to configure the classpath for the JRE yourself:

java -cp "/etc/myapp:/usr/local/lib/myapp.jar" myapp.core $*

Also, alternatively you can in fact leave all the resources included in the jar. That way the included resources would be used as defaults. If you set up the classpath so that the .jar file comes after the configuration folder (/etc/myapp in the example), then the resource files with the same relative resource path under the configuration folder will take precedence over the ones included in the jar.

Community
  • 1
  • 1
Daniel Dinnyes
  • 4,898
  • 3
  • 32
  • 44
16

Leiningen borrows the convention for resources from maven, with slightly different folder layouts. The rule states that the resources folder must be used as a compile time classpath root, meaning that leiningen is right in putting all the files inside resources folder in the root location inside the jar.

The problem is that physical location != classpath location, so that while the former changes when you package you application, the latter stays the same (classpath:/)

You'd better rely on classpath location to find your file system resources, or take them out of the jar and place them into a predictable folder, either a static or a configurable one.

skuro
  • 13,414
  • 1
  • 48
  • 67
6

I had the exact same problem and the solution is not using io/file at all. So for example this is working code from my app:

(defn load-md [md]
  (->
   md
   io/resource ;;clojure.java.io
   slurp
   mp
   to-hiccup
   html))
Adam Arold
  • 29,285
  • 22
  • 112
  • 207
3

Nice way of doing it. Note that io/file returns a java File object, which can then be passed to slurp, etc:

(ns rescue.core
  (:require [clojure.java.io :as io] ))

(def data-file (io/file
                 (io/resource 
                   "hello.txt" )))
(defn -main []
  (println (slurp data-file)) )

If you then execute the following in your lein project dir:

> echo "Hello Resources!" > resources/hello.txt
> lein run
Hello Resources!

and presto! You are reading resource files via the classpath.

Alan Thompson
  • 29,276
  • 6
  • 41
  • 48
2

In our case, using .getPath or .getFile was unhelpful. Simply using input-stream instead resolved it.

(require '[clojure.java.io :as io])

(defonce classifier
  (-> "machine_learning/classifier.model"
      io/resource
      io/input-stream
      helper/read))
Brad Koch
  • 19,267
  • 19
  • 110
  • 137
  • Please be more specific. You probably mean `clojure.java.io/resource`, but `resource` could be anything. The same goes for `input-stream`. And `helper/read` is not defined at all... – mzuther Jul 30 '20 at 06:21
  • @mzuther Sorry, I assumed usage of clojure.java.io, noted that explicitly now. And you're correct, helper/read could be anything; whatever function you want to consume the resource. – Brad Koch Jul 30 '20 at 18:10
  • @Brad_Koch Thanks! I like answers that stand for themselves and do not need much cross-reading. :) – mzuther Jul 31 '20 at 19:06
2

changing from

(.getPath (io/resource "file")) 

to ->

(str (io/resource "file"))

made it work in my case, as str adds file:/....path.... to path.

a.k
  • 1,035
  • 10
  • 27