You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@groovy.apache.org by pa...@apache.org on 2022/11/15 02:35:05 UTC

[groovy] branch GROOVY_4_0_X updated: GROOVY-10822: Groovysh DocCommand improvements

This is an automated email from the ASF dual-hosted git repository.

paulk pushed a commit to branch GROOVY_4_0_X
in repository https://gitbox.apache.org/repos/asf/groovy.git


The following commit(s) were added to refs/heads/GROOVY_4_0_X by this push:
     new 6932584b7b GROOVY-10822: Groovysh DocCommand improvements
6932584b7b is described below

commit 6932584b7b9f678ac7cb780bfbd1c7587eba78f7
Author: Paul King <pa...@asert.com.au>
AuthorDate: Sun Nov 13 23:54:47 2022 +1000

    GROOVY-10822: Groovysh DocCommand improvements
---
 .../groovy/groovysh/commands/DocCommand.groovy     | 68 ++++++++++++++--------
 .../groovy/groovysh/commands/DocCommand.properties |  2 +-
 .../groovy-groovysh/src/spec/doc/groovysh.adoc     | 61 +++++++++++++++++--
 .../groovy/groovysh/commands/DocCommandTest.groovy | 15 +++--
 4 files changed, 111 insertions(+), 35 deletions(-)

diff --git a/subprojects/groovy-groovysh/src/main/groovy/org/apache/groovy/groovysh/commands/DocCommand.groovy b/subprojects/groovy-groovysh/src/main/groovy/org/apache/groovy/groovysh/commands/DocCommand.groovy
index ecfaa9cbab..2f1bfb337a 100644
--- a/subprojects/groovy-groovysh/src/main/groovy/org/apache/groovy/groovysh/commands/DocCommand.groovy
+++ b/subprojects/groovy-groovysh/src/main/groovy/org/apache/groovy/groovysh/commands/DocCommand.groovy
@@ -33,6 +33,7 @@ class DocCommand extends CommandSupport {
 
     private static final String ENV_BROWSER = 'BROWSER'
     private static final String ENV_BROWSER_GROOVYSH = 'GROOVYSH_BROWSER'
+    private static final List<String> PRIMITIVES = ['boolean', 'byte', 'short', 'char', 'int', 'long', 'float', 'double']
 
     private static final int TIMEOUT_CONN = 5 * 1000 // ms
     private static final int TIMEOUT_READ = 5 * 1000 // ms
@@ -75,12 +76,17 @@ class DocCommand extends CommandSupport {
             doc(args[0])
             return
         }
+        if (args?.size() == 2) {
+            doc(args[1], args[0])
+            return
+        }
         fail(messages.format('error.unexpected_args', args ? args.join(' ') : 'no arguments'))
     }
 
-    void doc(String className) {
+    void doc(String className, String module = null) {
         def normalizedClassName = normalizeClassName(className)
-        def urls = urlsFor(normalizedClassName)
+        def normalizedModule = normalizeClassName(module ?: '')
+        def urls = urlsFor(normalizedClassName, normalizedModule)
         if (urls.empty) {
             fail("Documentation for \"${normalizedClassName}\" could not be found.")
         }
@@ -93,7 +99,7 @@ class DocCommand extends CommandSupport {
     }
 
     protected String normalizeClassName(String className) {
-        className.replace('"', '').replace("'", '')
+        className.replace('"', '').replace("'", '').replace('[', '%5B').replace(']', '%5D')
     }
 
     protected void browse(List urls) {
@@ -134,53 +140,65 @@ class DocCommand extends CommandSupport {
         }
     }
 
-    protected List urlsFor(String className) {
+    protected List urlsFor(String className, String module = '') {
         String groovyVersion = GroovySystem.version
-        def path = className.replace('.', '/') + '.html'
+        String path = className.replace('.', '/') + '.html'
 
         def urls = []
-        if (className.matches(/^(groovy|org\.codehaus\.groovy|)\..+/)) {
-            def url = new URL("http://docs.groovy-lang.org/$groovyVersion/html/gapi/$path")
-            if (sendHEADRequest(url)) {
+        if (!module && className.matches(/^(groovy|org\.codehaus\.groovy|org\.apache\.groovy|)\..+/)) {
+            def url = new URL("https://docs.groovy-lang.org/$groovyVersion/html/gapi/$path")
+            if (sendHEADRequest(url, path)) {
                 urls << url
             }
-        } else {
-            // Don't specify package names to not depend on a specific version of Java SE.
-            // Java SE includes non-java(x) packages such as org.w3m.*, org.omg.*. org.xml.* for now
-            // and new packages might be added in the future.
-            def url = new URL("http://docs.oracle.com/javase/${simpleVersion()}/docs/api/$path")
-            if (sendHEADRequest(url)) {
+        }
+        // Don't specify package names to not depend on a specific version of Java SE.
+        // Java SE includes non-java(x) packages such as org.w3m.*, org.omg.*. org.xml.* for now
+        // and new packages might be added in the future.
+        def url = new URL("https://docs.oracle.com/${versionPrefix(module)}/$path")
+        if (sendHEADRequest(url, path)) {
+            urls << url
+        } else if (!module) {
+            // if no module specified, fall back to JDK8 if java.base url wasn't found
+            url = new URL("https://docs.oracle.com/javase/8/docs/api/$path")
+            if (sendHEADRequest(url, path)) {
                 urls << url
-                url = new URL("http://docs.groovy-lang.org/$groovyVersion/html/groovy-jdk/$path")
-                if (sendHEADRequest(url)) {
-                    urls << url
-                }
             }
         }
+        // make accessing enhancements for e.g. int[] or double[][] easier
+        if (PRIMITIVES.any{path.startsWith(it) }) {
+            path = "primitives-and-primitive-arrays/$path"
+        }
+        url = new URL("https://docs.groovy-lang.org/$groovyVersion/html/groovy-jdk/$path")
+        if (sendHEADRequest(url, path)) {
+            urls << url
+        }
 
         urls
     }
 
-    private static simpleVersion() {
+    private static versionPrefix(String module) {
         String javaVersion = System.getProperty('java.version')
         if (javaVersion.startsWith('1.')) {
-            javaVersion.split(/\./)[1]
+            'javase/' + javaVersion.split(/\./)[1] + '/docs/api'
         } else {
             // java 9 and above
-            javaVersion.replaceAll(/-.*/, '').split(/\./)[0]
+            def mod = module ?: 'java.base'
+            'en/java/javase/' + javaVersion.replaceAll(/-.*/, '').split(/\./)[0] + "/docs/api/$mod"
         }
     }
 
-    protected boolean sendHEADRequest(URL url) {
+    protected boolean sendHEADRequest(URL url, String path = null) {
         try {
             HttpURLConnection conn = url.openConnection() as HttpURLConnection
             conn.requestMethod = 'HEAD'
             conn.connectTimeout = TIMEOUT_CONN
             conn.readTimeout = TIMEOUT_READ
             conn.instanceFollowRedirects = true
-
-            return conn.responseCode == 200
-
+            def code = conn.responseCode
+            // if not found, redirects to search page, which we don't count as successful
+            // if no path given (legacy calls from third parties), treat all redirects as suspicious
+            boolean successfulRedirect = path ? conn.URL.toString().endsWith(path) : url.toString() == conn.URL.toString()
+            return code == 200 && successfulRedirect
         } catch (IOException e) {
             fail "Sending a HEAD request to $url failed (${e}). Please check your network settings."
         }
diff --git a/subprojects/groovy-groovysh/src/main/resources/org/apache/groovy/groovysh/commands/DocCommand.properties b/subprojects/groovy-groovysh/src/main/resources/org/apache/groovy/groovysh/commands/DocCommand.properties
index a8498c4bc3..b8ea877c53 100644
--- a/subprojects/groovy-groovysh/src/main/resources/org/apache/groovy/groovysh/commands/DocCommand.properties
+++ b/subprojects/groovy-groovysh/src/main/resources/org/apache/groovy/groovysh/commands/DocCommand.properties
@@ -17,5 +17,5 @@
 #  under the License.
 #
 command.description=Open a browser window displaying the doc for the argument
-command.usage=[<class>]
+command.usage=[<module>] <class>
 command.help=Opens a browser window displaying the doc for the argument.
diff --git a/subprojects/groovy-groovysh/src/spec/doc/groovysh.adoc b/subprojects/groovy-groovysh/src/spec/doc/groovysh.adoc
index 200412d6c1..a9acc27b8d 100644
--- a/subprojects/groovy-groovysh/src/spec/doc/groovysh.adoc
+++ b/subprojects/groovy-groovysh/src/spec/doc/groovysh.adoc
@@ -395,19 +395,72 @@ Create an alias.
 [[GroovyShell-doc]]
 ===== `doc`
 
-Opens a browser with documentation for the provided class. For example:
+Opens a browser with documentation for the provided class.
 
+For example, we can get both the Javadoc and GDK enhancements doc for `java.util.List` (shown running on JDK17):
+
+[subs=attributes]
 ----
 groovy:000> :doc java.util.List
-http://docs.oracle.com/javase/7/docs/api/java/util/List.html
-http://docs.groovy-lang.org/2.4.2-SNAPSHOT/html/groovy-jdk/java/util/List.html
+https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/List.html
+https://docs.groovy-lang.org/{groovy-full-version}/html/groovy-jdk/java/util/List.html
 ----
 
-will open two windows (or tabs, depending on your browser):
+This will print the documentation URLs found and open two windows (or tabs, depending on your browser):
 
 * one for the JDK documentation
 * one for the GDK documentation
 
+By default, for Java classes, the `java.base` module is assumed. You can specify an optional module
+for other cases (shown running on JDK17):
+
+----
+groovy:000> :doc java.scripting javax.script.ScriptContext
+https://docs.oracle.com/en/java/javase/17/docs/api/java.scripting/javax/script/ScriptContext.html
+----
+
+For backwards compatibility, if no module is specified when searching for Java classes, and no class is found in the `java.base` module, an additional attempt is made to find documentation for the class in the JDK8 (pre-module) Javadoc:
+
+----
+groovy:000> :doc javax.script.ScriptContext
+https://docs.oracle.com/javase/8/docs/api/javax/script/ScriptContext.html
+----
+
+To get the Groovydoc for `groovy.ant.AntBuilder` and `groovy.xml.XmlSlurper`:
+
+[subs=attributes]
+----
+groovy:000> :doc groovy.ant.AntBuilder
+https://docs.groovy-lang.org/{groovy-full-version}/html/gapi/groovy/ant/AntBuilder.html
+groovy:000> :doc groovy.xml.XmlSlurper
+https://docs.groovy-lang.org/{groovy-full-version}/html/gapi/groovy/xml/XmlSlurper.html
+----
+
+To get both the Groovydoc and GDK enhancements doc for `groovy.lang.Closure` and `groovy.sql.GroovyResultSet`:
+
+[subs=attributes]
+----
+groovy:000> :doc groovy.lang.Closure
+https://docs.groovy-lang.org/{groovy-full-version}/html/gapi/groovy/lang/Closure.html
+https://docs.groovy-lang.org/{groovy-full-version}/html/groovy-jdk/groovy/lang/Closure.html
+groovy:000> :doc groovy.sql.GroovyResultSet
+https://docs.groovy-lang.org/{groovy-full-version}/html/gapi/groovy/sql/GroovyResultSet.html
+https://docs.groovy-lang.org/{groovy-full-version}/html/groovy-jdk/groovy/sql/GroovyResultSet.html
+----
+
+Documentation is also available for the GDK enhancements to primitive arrays and arrays of arrays:
+
+[subs=attributes]
+----
+groovy:000> :doc int[]
+https://docs.groovy-lang.org/{groovy-full-version}/html/groovy-jdk/primitives-and-primitive-arrays/int%5B%5D.html
+groovy:000> :doc double[][]
+https://docs.groovy-lang.org/{groovy-full-version}/html/groovy-jdk/primitives-and-primitive-arrays/double%5B%5D%5B%5D.html
+----
+
+NOTE: In contexts where opening a browser may not be desirable, e.g. on a CI server,
+this command can be disabled by setting the `groovysh.disableDocCommand` system property to `true`.
+
 [[GroovyShell-set]]
 ===== `set`
 
diff --git a/subprojects/groovy-groovysh/src/test/groovy/org/apache/groovy/groovysh/commands/DocCommandTest.groovy b/subprojects/groovy-groovysh/src/test/groovy/org/apache/groovy/groovysh/commands/DocCommandTest.groovy
index fa9f2c6ad6..1ce13d17c9 100644
--- a/subprojects/groovy-groovysh/src/test/groovy/org/apache/groovy/groovysh/commands/DocCommandTest.groovy
+++ b/subprojects/groovy-groovysh/src/test/groovy/org/apache/groovy/groovysh/commands/DocCommandTest.groovy
@@ -36,10 +36,15 @@ class DocCommandTest extends CommandTestSupport {
     void testUrlsForJavaOnlyClass() {
         def command = docCommandWithSendHEADRequestReturnValueOf { !it.host.contains('docs.groovy-lang.org') }
 
+        // no module specified
         def urls = command.urlsFor('org.ietf.jgss.GSSContext')
+        assert urls ==
+            [new URL("https://docs.oracle.com/javase/8/docs/api/org/ietf/jgss/GSSContext.html")]
 
+        // explicit module given
+        urls = command.urlsFor('org.ietf.jgss.GSSContext', 'java.security.jgss')
         assert urls ==
-            [new URL("http://docs.oracle.com/javase/${simpleJavaVersion()}/docs/api/org/ietf/jgss/GSSContext.html")]
+            [new URL("https://docs.oracle.com/en/java/javase/${simpleJavaVersion()}/docs/api/java.security.jgss/org/ietf/jgss/GSSContext.html")]
     }
 
     void testUrlsForJavaClass() {
@@ -48,17 +53,17 @@ class DocCommandTest extends CommandTestSupport {
         def urls = command.urlsFor('java.util.List')
 
         assert urls ==
-                [new URL("http://docs.oracle.com/javase/${simpleJavaVersion()}/docs/api/java/util/List.html"),
-                 new URL("http://docs.groovy-lang.org/$GroovySystem.version/html/groovy-jdk/java/util/List.html")]
+                [new URL("https://docs.oracle.com/en/java/javase/${simpleJavaVersion()}/docs/api/java.base/java/util/List.html"),
+                 new URL("https://docs.groovy-lang.org/$GroovySystem.version/html/groovy-jdk/java/util/List.html")]
     }
 
     void testUrlsForGroovyClass() {
         def command = docCommandWithSendHEADRequestReturnValueOf { true }
 
-        def urls = command.urlsFor('groovy.Dummy')
+        def urls = command.urlsFor('groovy.console.TextNode')
 
         assert urls ==
-                [new URL("http://docs.groovy-lang.org/$GroovySystem.version/html/gapi/groovy/Dummy.html")]
+                [new URL("https://docs.groovy-lang.org/$GroovySystem.version/html/gapi/groovy/console/TextNode.html")]
     }
 
     void testUrlsForWithUnknownClass() {