From 6d8e936a9f98e7f0339efc9bb4174490872ac64d Mon Sep 17 00:00:00 2001 From: Arne Brasseur Date: Mon, 27 Sep 2021 10:52:06 +0200 Subject: [PATCH] Include the resource-url in the java.parser result 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. --- src-jdk8/orchard/java/legacy_parser.clj | 34 ++++----------- src-newer-jdks/orchard/java/parser.clj | 44 ++++++------------- src/orchard/info.clj | 4 ++ src/orchard/java.clj | 6 ++- src/orchard/java/resource.clj | 6 ++- src/orchard/util/io.clj | 56 ++++++++++++++++++++++++- test/orchard/java/parser_test.clj | 16 +++---- test/orchard/java_test.clj | 2 +- 8 files changed, 95 insertions(+), 73 deletions(-) diff --git a/src-jdk8/orchard/java/legacy_parser.clj b/src-jdk8/orchard/java/legacy_parser.clj index 4c7ed2888..6b24796ff 100644 --- a/src-jdk8/orchard/java/legacy_parser.clj +++ b/src-jdk8/orchard/java/legacy_parser.clj @@ -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) @@ -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 @@ -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 _))) diff --git a/src-newer-jdks/orchard/java/parser.clj b/src-newer-jdks/orchard/java/parser.clj index 78b7544c8..a72ee7754 100644 --- a/src-newer-jdks/orchard/java/parser.clj +++ b/src-newer-jdks/orchard/java/parser.clj @@ -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) @@ -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 @@ -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 _))) diff --git a/src/orchard/info.clj b/src/orchard/info.clj index 91a77a367..ee4d82f80 100644 --- a/src/orchard/info.clj +++ b/src/orchard/info.clj @@ -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")) diff --git a/src/orchard/java.clj b/src/orchard/java.clj index 4cca70b66..13566960c 100644 --- a/src/orchard/java.clj +++ b/src/orchard/java.clj @@ -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. diff --git a/src/orchard/java/resource.clj b/src/orchard/java/resource.clj index 0dc2a508d..4622365ad 100644 --- a/src/orchard/java/resource.clj +++ b/src/orchard/java/resource.clj @@ -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] @@ -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)))))) diff --git a/src/orchard/util/io.clj b/src/orchard/util/io.clj index b2255276e..1f756cce7 100644 --- a/src/orchard/util/io.clj +++ b/src/orchard/util/io.clj @@ -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`. @@ -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))) diff --git a/test/orchard/java/parser_test.clj b/test/orchard/java/parser_test.clj index 9bf6d31d7..5fb497f96 100644 --- a/test/orchard/java/parser_test.clj +++ b/test/orchard/java/parser_test.clj @@ -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)))))))) diff --git a/test/orchard/java_test.clj b/test/orchard/java_test.clj index fd5b3ceff..e7de835f2 100644 --- a/test/orchard/java_test.clj +++ b/test/orchard/java_test.clj @@ -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)