blob: ec473c8329a90cecac4e9962aedae3846c502b68 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.groovy.groovysh.commands
import jline.console.completer.Completer
import org.apache.groovy.groovysh.CommandSupport
import org.apache.groovy.groovysh.Groovysh
/**
* The 'doc' command.
*
* @since 2.2.0
*/
class DocCommand extends CommandSupport {
public static final String COMMAND_NAME = ':doc'
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
// indicates support for java.awt.Desktop#browse on the current platform
private static boolean hasAWTDesktopPlatformSupport
private static desktop
/**
* Check for java.awt.Desktop#browse platform support
*/
static {
try {
def desktopClass = Class.forName('java.awt.Desktop')
desktop = desktopClass.desktopSupported ? desktopClass.desktop : null
hasAWTDesktopPlatformSupport =
desktop != null &&
desktop.isSupported(desktopClass.declaredClasses.find { it.simpleName == 'Action' }.BROWSE)
} catch (Exception e) {
hasAWTDesktopPlatformSupport = false
desktop = null
}
}
DocCommand(final Groovysh shell) {
super(shell, COMMAND_NAME, ':D')
}
@Override
protected List<Completer> createCompleters() {
return [new ImportCompleter(shell.packageHelper, shell.interp, false),
null]
}
@Override
Object execute(final List<String> args) {
if (args?.size() == 1) {
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, String module = null) {
def normalizedClassName = normalizeClassName(className)
def normalizedModule = normalizeClassName(module ?: '')
def urls = urlsFor(normalizedClassName, normalizedModule)
if (urls.empty) {
fail("Documentation for \"${normalizedClassName}\" could not be found.")
}
// Print the URLs.
// It is useful especially when the browsing fails.
urls.each { url -> io.out.println url }
browse(urls)
}
protected String normalizeClassName(String className) {
className.replace('"', '').replace("'", '').replace('[', '%5B').replace(']', '%5D')
}
protected void browse(List urls) {
def browser = browserEnvironmentVariable
// fallback to java.awt.Desktop in case of missing env variable(s)
if (browser) {
browseWithNativeBrowser(browser, urls)
} else if (hasAWTDesktopPlatformSupport) {
browseWithAWT(urls)
} else {
fail 'Browser could not be opened due to missing platform support for "java.awt.Desktop". Please set ' +
"a $ENV_BROWSER_GROOVYSH or $ENV_BROWSER environment variable referring to the browser binary to " +
'solve this issue.'
}
}
protected String getBrowserEnvironmentVariable() {
System.getenv(ENV_BROWSER_GROOVYSH) ?: System.getenv(ENV_BROWSER)
}
protected void browseWithAWT(List urls) {
try {
urls.each { url -> desktop.browse(url.toURI()) }
} catch (Exception e) {
fail "Browser could not be opened, an unexpected error occured (${e}). You can add a " +
"$ENV_BROWSER_GROOVYSH or $ENV_BROWSER environment variable to explicitly specify a browser binary."
}
}
protected void browseWithNativeBrowser(String browser, List urls) {
try {
"$browser ${urls.join(' ')}".execute()
} catch (Exception e) {
// we could be here caused by a IOException, SecurityException or NP Exception
fail "Browser could not be opened (${e}). Please check the $ENV_BROWSER_GROOVYSH or $ENV_BROWSER " +
"environment variable."
}
}
protected List urlsFor(String className, String module = '') {
String groovyVersion = GroovySystem.getVersion()
String path = className.replace('.', '/') + '.html'
def url
def urls = []
if (!module && className.matches(/^(?:org\.(?:apache|codehaus)\.)?groovy\..+/)) {
if (sendHEADRequest(url = new URL("https://docs.groovy-lang.org/$groovyVersion/html/gapi/$path"), path)) {
urls << 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.omg.*, org.w3c.*, org.xml.* for now
// and new packages might be added in the future.
if (sendHEADRequest(url = new URL("https://docs.oracle.com/${versionPrefix(module)}/$path"), path) ||
sendHEADRequest(url = new URL("https://download.java.net/java/early_access/${versionPrefix(module, true)}/$path"), path)) {
urls << url
} else if (!module) {
// if no module specified, fall back to JDK8 if java.base url wasn't found
if (sendHEADRequest(url = new URL("https://docs.oracle.com/javase/8/docs/api/$path"), path)) {
urls << url
}
}
// make accessing enhancements for e.g. int[] or double[][] easier
if (PRIMITIVES.any{path.startsWith(it) }) {
path = "primitives-and-primitive-arrays/$path"
}
if (sendHEADRequest(url = new URL("https://docs.groovy-lang.org/$groovyVersion/html/groovy-jdk/$path"), path)) {
urls << url
}
urls
}
private static versionPrefix(String module, boolean ea = false) {
String javaVersion = System.getProperty('java.version')
if (javaVersion.startsWith('1.')) {
'javase/' + javaVersion.split(/\./)[1] + '/docs/api'
} else {
// java 9 and above
def mod = module ?: 'java.base'
def ver = javaVersion.replaceAll(/-.*/, '').split(/\./)[0]
"${(ea ? 'jdk' : 'en/java/javase/')}$ver/docs/api/$mod"
}
}
protected boolean sendHEADRequest(URL url, String path = null) {
IOException ioe
// try at most 3 times
for (int i = 0; i < 3; i++) {
try {
return doSendHEADRequest(url, path)
} catch (SocketTimeoutException e) {
ioe = e
} catch (IOException e) {
ioe = e
break
}
}
io.out.println "Sending a HEAD request to $url failed (${ioe}). Please check your network settings."
// allow timeout to fail since this will happen if we check e.g. for an early access URL for a release JDK version
if (ioe !instanceof SocketTimeoutException) fail "Unable to get URLs for documentation."
return false
}
private boolean doSendHEADRequest(URL url, String path = null) {
HttpURLConnection conn = null
try {
conn = (HttpURLConnection) url.openConnection()
conn.setInstanceFollowRedirects(true)
conn.setConnectTimeout(TIMEOUT_CONN)
conn.setReadTimeout(TIMEOUT_READ)
conn.setRequestMethod('HEAD')
// 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
String connectionURL = conn.getURL().toString()
boolean successfulRedirect = path ? connectionURL.endsWith(path) : connectionURL.equals(url.toString())
return (conn.getResponseCode() == HttpURLConnection.HTTP_OK) && (conn.getContentLength() > 0) && successfulRedirect
} finally {
conn?.disconnect()
}
}
}