Skip to content

Fix open help with new menu item "Source or javadoc for class or package..." #46

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Nov 29, 2020
240 changes: 240 additions & 0 deletions src/main/java/org/scijava/ui/swing/script/ClassUtil.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
package org.scijava.ui.swing.script;

import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Scanner;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class ClassUtil {

static private final String scijava_javadoc_URL = "https://javadoc.scijava.org/"; // with ending slash

/** Cache of class names vs list of URLs found in the pom.xml files of their contaning jar files, if any. */
static private final Map<String, JarProperties> class_urls = new HashMap<>();

/** Cache of subURL javadoc at https://javadoc.scijava.org */
static private final HashMap<String, String> scijava_javadoc_URLs = new HashMap<>();

static private final void ensureCache() {
synchronized (class_urls) {
if (class_urls.isEmpty()) {
final ArrayList<String> dirs = new ArrayList<>();
dirs.add(System.getProperty("java.home"));
dirs.add(System.getProperty("ij.dir"));
class_urls.putAll(findAllClasses(dirs));
}
}
}

static public final void ensureSciJavaSubURLCache() {
synchronized (scijava_javadoc_URLs) {
if (!scijava_javadoc_URLs.isEmpty()) return;
Scanner scanner = null;
try {
final Pattern pattern = Pattern.compile("<div class=\"jdbox\"><div><a href=\"(.*?)\">");
final URLConnection connection = new URL(scijava_javadoc_URL).openConnection();
scanner = new Scanner(connection.getInputStream());
while (scanner.hasNext()) {
final Matcher matcher = pattern.matcher(scanner.nextLine());
if (matcher.find()) {
String name = matcher.group(1).toLowerCase();
if (name.endsWith("/")) name = name.substring(0, name.length() -1);
scijava_javadoc_URLs.put(name, scijava_javadoc_URL + matcher.group(1));
}
}
scanner.close();
} catch ( Exception e ) {
e.printStackTrace();
} finally {
if (null != scanner) scanner.close();
}
}
}

static public HashMap<String, JarProperties> findClassDocumentationURLs(final String s) {
ensureCache();
final HashMap<String, JarProperties> matches = new HashMap<>();
for (final Map.Entry<String, JarProperties> entry: class_urls.entrySet()) {
if (entry.getKey().contains(s)) {
final JarProperties props = entry.getValue();
matches.put(entry.getKey(), new JarProperties(props.name, new ArrayList<String>(props.urls)));
}
}
return matches;
}

static public HashMap<String, ArrayList<String>> findDocumentationForClass(final String s) {
final HashMap<String, JarProperties> matches = findClassDocumentationURLs(s);
ensureSciJavaSubURLCache();

final Pattern javaPackages = Pattern.compile("^(java|javax|org\\.omg|org\\.w3c|org\\.xml|org\\.ietf\\.jgss)\\..*$");
final String version = System.getProperty("java.version");
final String majorVersion = version.startsWith("1.") ?
version.substring(2, version.indexOf('.', 2))
: version.substring(0, version.indexOf('.'));
final String javaDoc = "java" + majorVersion;

final HashMap<String, ArrayList<String>> class_urls = new HashMap<>();

for (final Map.Entry<String, JarProperties> entry: matches.entrySet()) {
final String classname = entry.getKey();
final ArrayList<String> urls = new ArrayList<>();
class_urls.put(classname, urls);
if (javaPackages.matcher(classname).matches()) {
urls.add(scijava_javadoc_URLs.get(javaDoc) + classname.replace('.', '/') + ".html");
} else {
final JarProperties props = entry.getValue();
// Find the first URL with git in it
for (final String url : props.urls) {
final boolean github = url.contains("/github.com"),
gitlab = url.contains("/gitlab.com");
if (github || gitlab) {
// Find the 5th slash, e.g. https://github.com/imglib/imglib2/
int count = 0;
int last = 0;
while (count < 5) {
last = url.indexOf('/', last + 1);
if (-1 == last) break; // less than 5 found
++count;
}
String urlbase = url;
if (5 == count) urlbase = url.substring(0, last); // without the ending slash
// Assume maven, since these URLs were found in a pom.xml: src/main/java/
urls.add(urlbase + (gitlab ? "/-" : "") + "/blob/master/src/main/java/" + classname.replace('.', '/') + ".java");
break;
}
}
// Try to find a javadoc in the scijava website
if (null != props.name) {
String scijava_javadoc_url = scijava_javadoc_URLs.get(props.name.toLowerCase());
if (null == scijava_javadoc_url) {
// Try cropping name at the first whitespace if any (e.g. "ImgLib2 Core Library" to "ImgLib2")
for (final String word: props.name.split(" ")) {
scijava_javadoc_url = scijava_javadoc_URLs.get(word.toLowerCase());
if (null != scijava_javadoc_url) break; // found a valid one
}
}
if (null != scijava_javadoc_url) {
urls.add(scijava_javadoc_url + classname.replace('.', '/') + ".html");
} else {
// Try Fiji: could be a plugin
Scanner scanner = null;
try {
final String url = scijava_javadoc_URL + "Fiji/" + classname.replace('.', '/') + ".html";
final URLConnection c = new URL(url).openConnection();
scanner = new Scanner(c.getInputStream());
while (scanner.hasNext()) {
final String line = scanner.nextLine();
if (line.contains("<title>")) {
if (!line.contains("<title>404")) {
urls.add(url);
}
break;
}
}
} catch (Exception e) {
// Ignore: 404 that wasn't redirected to an error page
} finally {
if (null != scanner) scanner.close();
}
}
}
}
}

return class_urls;
}

static public final class JarProperties {
public final ArrayList<String> urls;
public String name = null;
public JarProperties(final String name, final ArrayList<String> urls) {
this.name = name;
this.urls = urls;
}
}

static public final HashMap<String, JarProperties> findAllClasses(final List<String> jar_folders) {
// Find all jar files
final ArrayList<String> jarFilePaths = new ArrayList<String>();
final LinkedList<String> dirs = new LinkedList<>(jar_folders);
final HashSet<String> seenDirs = new HashSet<>();
while (!dirs.isEmpty()) {
final String filepath = dirs.removeFirst();
if (null == filepath) continue;
final File file = new File(filepath);
seenDirs.add(file.getAbsolutePath());
if (file.exists() && file.isDirectory()) {
for (final File child : file.listFiles()) {
final String childfilepath = child.getAbsolutePath();
if (seenDirs.contains(childfilepath)) continue;
if (child.isDirectory()) dirs.add(childfilepath);
else if (childfilepath.endsWith(".jar")) jarFilePaths.add(childfilepath);
}
}
}
// Find all classes from all jar files
final HashMap<String, JarProperties> class_urls = new HashMap<>();
final Pattern urlpattern = Pattern.compile(">(http.*?)<");
final Pattern namepattern = Pattern.compile("<name>(.*?)<");
for (final String jarpath : jarFilePaths) {
JarFile jar = null;
try {
jar = new JarFile(jarpath);
final Enumeration<JarEntry> entries = jar.entries();
final ArrayList<String> urls = new ArrayList<>();
final JarProperties props = new JarProperties(null, urls);
// For every filepath in the jar zip archive
while (entries.hasMoreElements()) {
final JarEntry entry = entries.nextElement();
if (entry.isDirectory()) continue;
if (entry.getName().endsWith(".class")) {
String classname = entry.getName().replace('/', '.');
final int idollar = classname.indexOf('$');
if (-1 != idollar) {
classname = classname.substring(0, idollar); // truncate at the first dollar sign
} else {
classname = classname.substring(0, classname.length() - 6); // without .class
}
class_urls.put(classname, props);
} else if (entry.getName().endsWith("/pom.xml")) {
final Scanner scanner = new Scanner(jar.getInputStream(entry));
while (scanner.hasNext()) {
final String line = scanner.nextLine();
final Matcher matcher1 = urlpattern.matcher(line);
if (matcher1.find()) {
urls.add(matcher1.group(1));
}
if (null == props.name) {
final Matcher matcher2 = namepattern.matcher(line);
if (matcher2.find()) {
props.name = matcher2.group(1);
}
}
}
scanner.close();
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (null != jar) try {
jar.close();
} catch (IOException e) { e.printStackTrace(); }
}
}
return class_urls;
}
}
95 changes: 93 additions & 2 deletions src/main/java/org/scijava/ui/swing/script/TextEditor.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
package org.scijava.ui.swing.script;

import java.awt.Color;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.GridBagConstraints;
Expand Down Expand Up @@ -108,6 +109,7 @@
import javax.swing.JCheckBoxMenuItem;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
Expand Down Expand Up @@ -214,7 +216,7 @@ public class TextEditor extends JFrame implements ActionListener,
openMacroFunctions, decreaseFontSize, increaseFontSize, chooseFontSize,
chooseTabSize, gitGrep, openInGitweb, replaceTabsWithSpaces,
replaceSpacesWithTabs, toggleWhiteSpaceLabeling, zapGremlins,
savePreferences, toggleAutoCompletionMenu;
savePreferences, toggleAutoCompletionMenu, openClassOrPackageHelp;
private RecentFilesMenuItem openRecent;
private JMenu gitMenu, tabsMenu, fontSizeMenu, tabSizeMenu, toolsMenu,
runMenu, whiteSpaceMenu;
Expand Down Expand Up @@ -483,6 +485,8 @@ public TextEditor(final Context context) {
openHelp =
addToMenu(toolsMenu, "Open Help for Class (with frames)...", 0, 0);
openHelp.setMnemonic(KeyEvent.VK_P);
openClassOrPackageHelp = addToMenu(toolsMenu, "Source or javadoc for class or package...", 0, 0);
openClassOrPackageHelp.setMnemonic(KeyEvent.VK_S);
openMacroFunctions =
addToMenu(toolsMenu, "Open Help on Macro Functions...", 0, 0);
openMacroFunctions.setMnemonic(KeyEvent.VK_H);
Expand Down Expand Up @@ -988,7 +992,12 @@ public void loadPreferences() {

final int windowWidth = prefService.getInt(getClass(), WINDOW_WIDTH, dim.width);
final int windowHeight = prefService.getInt(getClass(), WINDOW_HEIGHT, dim.height);
setPreferredSize(new Dimension(windowWidth, windowHeight));
// Avoid creating a window larger than the desktop
final Dimension screen = Toolkit.getDefaultToolkit().getScreenSize();
if (windowWidth > screen.getWidth() || windowHeight > screen.getHeight())
setPreferredSize(new Dimension(DEFAULT_WINDOW_WIDTH, DEFAULT_WINDOW_HEIGHT));
else
setPreferredSize(new Dimension(windowWidth, windowHeight));

final int mainDivLocation = prefService.getInt(getClass(), MAIN_DIV_LOCATION, body.getDividerLocation());
body.setDividerLocation(mainDivLocation);
Expand Down Expand Up @@ -1395,6 +1404,7 @@ else if (source == savePreferences) {
}
else if (source == openHelp) openHelp(null);
else if (source == openHelpWithoutFrames) openHelp(null, false);
else if (source == openClassOrPackageHelp) openClassOrPackageHelp(null);
else if (source == openMacroFunctions) try {
new MacroFunctions(this).openHelp(getTextArea().getSelectedText());
}
Expand Down Expand Up @@ -2659,6 +2669,87 @@ public void openHelp(String className, final boolean withFrames) {
handleException(e);
}
}

/**
* @param text Either a classname, or a partial class name, or package name or any part of the fully qualified class name.
*/
public void openClassOrPackageHelp(String text) {
if (text == null)
text = getSelectedClassNameOrAsk();
if (null == text) return;
new Thread(new FindClassSourceAndJavadoc(text)).start(); // fork away from event dispatch thread
}

public class FindClassSourceAndJavadoc implements Runnable {
private final String text;
public FindClassSourceAndJavadoc(final String text) {
this.text = text;
}
@Override
public void run() {
setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
final HashMap<String, ArrayList<String>> matches;
try {
matches = ClassUtil.findDocumentationForClass(text);
} finally {
setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
}
if (matches.isEmpty()) {
JOptionPane.showMessageDialog(getEditorPane(), "No info found for:\n'" + text +'"');
return;
}
final JPanel panel = new JPanel();
final GridBagLayout gridbag = new GridBagLayout();
final GridBagConstraints c = new GridBagConstraints();
panel.setLayout(gridbag);
panel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
final List<String> keys = new ArrayList<String>(matches.keySet());
Collections.sort(keys);
c.gridy = 0;
for (final String classname: keys) {
c.gridx = 0;
c.anchor = GridBagConstraints.EAST;
final JLabel class_label = new JLabel(classname);
gridbag.setConstraints(class_label, c);
panel.add(class_label);
ArrayList<String> urls = matches.get(classname);
if (urls.isEmpty()) {
urls = new ArrayList<String>();
urls.add("https://duckduckgo.com/?q=" + classname);
}
for (final String url: urls) {
c.gridx += 1;
c.anchor = GridBagConstraints.WEST;
String title = "JavaDoc";
if (url.endsWith(".java")) title = "Source";
else if (url.contains("duckduckgo")) title = "Search...";
final JButton link = new JButton(title);
gridbag.setConstraints(link, c);
panel.add(link);
link.addActionListener(new ActionListener() {
public void actionPerformed(final ActionEvent event) {
try {
platformService.open(new URL(url));
} catch (Exception e) {
e.printStackTrace();
}
}
});
}
c.gridy += 1;
}
final JScrollPane jsp = new JScrollPane(panel);
//jsp.setPreferredSize(new Dimension(800, 500));
SwingUtilities.invokeLater(new Runnable() {
public void run() {
final JFrame frame = new JFrame(text);
frame.getContentPane().add(jsp);
frame.pack();
frame.setVisible(true);
}
});
}
}

public void extractSourceJar() {
final File file = openWithDialog(null);
Expand Down