Skip to content

Commit

Permalink
Include the resource-url in the java.parser result
Browse files Browse the repository at this point in the history
java.parser/legacy-parser already does a `io/resource` lookup on the resource it
is parsing, meaning it has a full URL (jar: or file:). By including this URL in
the map it returns callers can do further checks or operations on the resource
without having to re-scan the classpath.

This adds some helpers to orchard.util.io for dealing with resource URLs in a
more transparent way.
  • Loading branch information
plexus committed Nov 5, 2021
1 parent d225c2f commit 6d8e936
Show file tree
Hide file tree
Showing 8 changed files with 95 additions and 73 deletions.
34 changes: 7 additions & 27 deletions src-jdk8/orchard/java/legacy_parser.clj
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
ModifierFilter RootDocImpl)
(com.sun.tools.javac.tree JCTree)
(java.io StringReader)
(java.net URL JarURLConnection)
(java.net URI)
(javax.swing.text.html HTML$Tag HTMLEditorKit$ParserCallback)
(javax.swing.text.html.parser ParserDelegator)
Expand Down Expand Up @@ -261,18 +260,6 @@
(str/replace "." "/")
(str ".java")))

(def url-protocol (memfn ^URL getProtocol))

(defn jar-path
"Given a jar:file:...!/... URL, return the location of the jar file on the
filesystem. Returns nil on any other URL."
[^URL jar-resource]
(when (= "jar" (url-protocol jar-resource))
(let [^JarURLConnection conn (.openConnection jar-resource)
inner-url (.getJarFileURL conn)]
(when (= "file" (url-protocol inner-url))
(.getPath inner-url)))))

(defn source-info
"If the source for the Java class is available on the classpath, parse it
and return info to supplement reflection. Specifically, this includes source
Expand All @@ -283,18 +270,11 @@
(try
(let [path (source-path klass)]
(when-let [root (parse-java path)]
(let [resource-url (io/resource path)
protocol (url-protocol resource-url)]
(assoc (->> (map parse-info (.classes root))
(filter #(= klass (:class %)))
(first))
;; relative path on the classpath
:file path
;; filesystem object for this resource, either the file
;; itself, or its jar archive
:path (case protocol
"file" (.getPath resource-url)
"jar" (jar-path resource-url))
;; is the resource inside an archive
:archive? (= "jar" protocol)))))
(assoc (->> (map parse-info (.classes root))
(filter #(= klass (:class %)))
(first))
;; relative path on the classpath
:file path
;; Full URL, e.g. file:.. or jar:...
:resource-url (io/resource path))))
(catch Abort _)))
44 changes: 12 additions & 32 deletions src-newer-jdks/orchard/java/parser.clj
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
[clojure.string :as str])
(:import
(java.io StringReader StringWriter)
(java.net URL JarURLConnection)
(javax.lang.model.element Element ElementKind ExecutableElement
TypeElement VariableElement)
(javax.swing.text.html HTML$Tag HTMLEditorKit$ParserCallback)
Expand Down Expand Up @@ -286,18 +285,6 @@
(str module "/" path)
path))))

(def url-protocol (memfn ^URL getProtocol))

(defn jar-path
"Given a jar:file:...!/... URL, return the location of the jar file on the
filesystem. Returns nil on any other URL."
[^URL jar-resource]
(when (= "jar" (url-protocol jar-resource))
(let [^JarURLConnection conn (.openConnection jar-resource)
inner-url (.getJarFileURL conn)]
(when (= "file" (url-protocol inner-url))
(.getPath inner-url)))))

(defn source-info
"If the source for the Java class is available on the classpath, parse it
and return info to supplement reflection. Specifically, this includes source
Expand All @@ -309,24 +296,17 @@
(when-let [path (source-path klass)]
(when-let [^DocletEnvironment root (parse-java path (module-name klass))]
(try
(let [resource-url (io/resource path)
protocol (url-protocol resource-url)]
(assoc (->> (.getIncludedElements root)
(filter #(#{ElementKind/CLASS
ElementKind/INTERFACE
ElementKind/ENUM}
(.getKind ^Element %)))
(map #(parse-info % root))
(filter #(= klass (:class %)))
(first))
;; relative path on the classpath
:file path
;; filesystem object for this resource, either the file
;; itself, or its jar archive
:path (case protocol
"file" (.getPath resource-url)
"jar" (jar-path resource-url))
;; is the resource inside an archive
:archive? (= "jar" protocol)))
(assoc (->> (.getIncludedElements root)
(filter #(#{ElementKind/CLASS
ElementKind/INTERFACE
ElementKind/ENUM}
(.getKind ^Element %)))
(map #(parse-info % root))
(filter #(= klass (:class %)))
(first))
;; relative path on the classpath
:file path
;; Full URL, e.g. file:.. or jar:...
:resource-url (io/resource path))
(finally (.close (.getJavaFileManager root))))))
(catch Throwable _)))
4 changes: 4 additions & 0 deletions src/orchard/info.clj
Original file line number Diff line number Diff line change
Expand Up @@ -195,3 +195,7 @@
classes. If no source is available, return the relative path as is."
[^String path]
{:javadoc (java/resolve-javadoc-path path)})

(comment
(info-java 'clojure.lang.RT 'baseLoader)
(file-info "clojure/core.clj"))
6 changes: 4 additions & 2 deletions src/orchard/java.clj
Original file line number Diff line number Diff line change
Expand Up @@ -259,9 +259,11 @@
info (if cached
(:info cached)
(class-info* class))
last-modified (if (or (:archive? info) (nil? (:path info)))
resource (:resource-url info)
last-modified (if (or (nil? resource)
(util.io/in-archive? resource))
0
(.lastModified ^File (io/file (:path info))))
(util.io/last-modified-time resource))
stale (not= last-modified (:last-modified cached))
;; If last-modified in cache mismatches last-modified of the file,
;; regenerate class-info.
Expand Down
6 changes: 4 additions & 2 deletions src/orchard/java/resource.clj
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@
s))

(defn project-resources
"Get a list of classpath resources."
"Get a list of classpath resources, i.e. files that are not clojure/java source
or class files. Only consider classpath entries that are directories, does not
consider jars."
[]
(mapcat
(fn [^File directory]
Expand All @@ -32,7 +34,7 @@
{:root directory
:file file
:relpath relpath
:url (io/resource relpath)})))
:url (io/as-url file)})))
(remove #(.startsWith ^String (:relpath %) "META-INF/"))
(remove #(re-matches #".*\.(clj[cs]?|java|class)" (:relpath %)))))
(filter (memfn ^File isDirectory) (map io/as-file (cp/classpath (cp/boot-aware-classloader))))))
Expand Down
56 changes: 55 additions & 1 deletion src/orchard/util/io.clj
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
(ns orchard.util.io)
(ns orchard.util.io
(:require
[clojure.java.io :as io])
(:import
(java.io File)
(java.net URL JarURLConnection)))

(defn wrap-silently
"Middleware that executes `(f)` without printing to `System/out` or `System/err`.
Expand All @@ -22,3 +27,52 @@
(System/setOut old-out))
(when (= ps System/err) ;; `System/err` may have changed in the meantime (in face of concurrency)
(System/setErr old-err)))))))

(defn url-protocol
"Get the URL protocol as a string, e.g. http, file, jar."
[^java.net.URL url]
(.getProtocol url))

(defn in-archive?
"Does this URL point to a file inside a jar (or zip) archive.
i.e. does it use the jar: protocol."
[^java.net.URL url]
(= "jar" (url-protocol url)))

(defn jar-location
"Given a jar:file:...!/... URL, return the location of the jar file on the
filesystem. Returns nil on any other URL."
^File [^URL jar-resource]
(assert (= "jar" (url-protocol jar-resource)))
(let [^JarURLConnection conn (.openConnection jar-resource)
inner-url (.getJarFileURL conn)]
(when (= "file" (url-protocol inner-url))
(io/as-file inner-url))))

(defn resource-artifact
"Get the fully qualified file name containing the resource identified by the
given file: or jar: URL. Returns either the location of the resource itself,
or of the archive containing the resource."
^File [^java.net.URL resource]
(let [protocol (url-protocol resource)]
(case protocol
"file"
(io/as-file resource)
"jar"
(jar-location resource)
#_else
(throw (ex-info (str "URLs with a " protocol
" protocol can't be situated on the filesystem.")
{:resource resource})))))

(defprotocol LastModifiedTime
(last-modified-time [this]
"Return the last modified time of a File or resource URL."))

(extend-protocol LastModifiedTime
java.net.URL
(last-modified-time [this]
(last-modified-time (resource-artifact this)))
java.io.File
(last-modified-time [this]
(.lastModified this)))
16 changes: 8 additions & 8 deletions test/orchard/java/parser_test.clj
Original file line number Diff line number Diff line change
Expand Up @@ -53,15 +53,15 @@
"Class level docstring.\n\n```\n DummyClass dc = new DummyClass();\n```\n\n@author Arne Brasseur",
:line 12,
:column 1,
:file "orchard/java/DummyClass.java",
:path (str (System/getProperty "user.dir")
"/test/orchard/java/DummyClass.java")
:archive? false}
:file "orchard/java/DummyClass.java"
:resource-url (java.net.URL. (str "file:"
(System/getProperty "user.dir")
"/test/orchard/java/DummyClass.java"))}
((resolve 'orchard.java.parser/source-info) 'orchard.java.DummyClass))))

(testing "java file in a jar"
(let [rt-info ((resolve 'orchard.java.parser/source-info) 'clojure.lang.RT)]
(is (= {:file "clojure/lang/RT.java" :archive? true}
(select-keys rt-info [:file :archive?])))
(is (re-find #"/.*/.m2/repository/org/clojure/clojure/.*/clojure-.*-sources.jar"
(:path rt-info)))))))
(is (= {:file "clojure/lang/RT.java"}
(select-keys rt-info [:file])))
(is (re-find #"jar:file:/.*/.m2/repository/org/clojure/clojure/.*/clojure-.*-sources.jar!/clojure/lang/RT.java"
(str (:resource-url rt-info))))))))
2 changes: 1 addition & 1 deletion test/orchard/java_test.clj
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@
(deftest map-structure-test
(when jdk-parser?
(testing "Parsed map structure = reflected map structure"
(let [cols #{:file :line :column :doc :argnames :argtypes :path :archive?}
(let [cols #{:file :line :column :doc :argnames :argtypes :resource-url}
keys= #(= (set (keys (apply dissoc %1 cols)))
(set (keys %2)))
c1 (class-info* 'clojure.lang.Compiler)
Expand Down

0 comments on commit 6d8e936

Please sign in to comment.