All Downloads are FREE. Search and download functionalities are using the official Maven repository.

net.imagej.patcher.LegacyHooks Maven / Gradle / Ivy

Go to download

A runtime patcher to introduce extension points into the original ImageJ (1.x). This project offers extension points for use with ImageJ2 and it also offers limited support for headless operations.

There is a newer version: 1.2.7
Show newest version
/*
 * #%L
 * ImageJ software for multidimensional image processing and analysis.
 * %%
 * Copyright (C) 2009 - 2016 Board of Regents of the University of
 * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
 * Institute of Molecular Cell Biology and Genetics.
 * %%
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * 
 * 1. Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 * #L%
 */

package net.imagej.patcher;

import java.awt.event.KeyEvent;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.jar.Attributes.Name;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import java.util.regex.Pattern;

/**
 * Extension points for ImageJ 1.x.
 * 

* These extension points will be patched into ImageJ 1.x by the * {@link CodeHacker}. To override the behavior of ImageJ 1.x, a new instance of * this class needs to be installed into ij.IJ._hooks. *

*

* The essential functionality of the hooks is provided in the * {@link EssentialLegacyHooks} class, which makes an excellent base class for * project-specific implementations. *

* * @author Johannes Schindelin */ public abstract class LegacyHooks { /** * Determines whether the image windows should be displayed or not. * * @return false if ImageJ 1.x should be prevented from opening image windows. */ public boolean isLegacyMode() { return true; } /** * Returns the current context, if any. *

* For ImageJ2-specific hooks, the returned object will be the current SciJava * context, or null if the context is not yet initialized. *

* * @return the context, or null */ public Object getContext() { return null; } /** * Allows interception of ImageJ 1.x's {@link ij.ImageJ#quit()} method. * * @return whether ImageJ 1.x should proceed with its usual quitting routine */ public boolean quit() { return true; } /** * Runs when the hooks are installed into an existing legacy environment. */ public void installed() { // ignore } /** * Disposes of the hooks. *

* This method is called when ImageJ 1.x is quitting or when new hooks are * installed. *

*/ public void dispose() { // ignore } /** * Intercepts the call to {@link ij.IJ#runPlugIn(String, String)}. * * @param className the class name * @param arg the argument passed to the {@code runPlugIn} method * @return the object to return, or null to let ImageJ 1.x handle the call */ public Object interceptRunPlugIn(final String className, final String arg) { return null; } /** * Updates the progress bar, where {@code 0 <= progress <= 1.0}. * * @param progress between 0.0 and 1.0 */ public void showProgress(final double progress) {} /** * Updates the progress bar, where the length of the bar is set to * {@code (currentValue + 1) / finalValue} of the maximum bar length. The bar * is erased if {@code currentValue >= finalValue}. * * @param currentIndex the step that was just started * @param finalIndex the final step. */ public void showProgress(final int currentIndex, final int finalIndex) {} /** * Shows a status message. * * @param status the message */ public void showStatus(final String status) {} /** * Logs a message. * * @param message the message */ public void log(final String message) {} /** * Registers an image (possibly not seen before). * * @param image the new image */ public void registerImage(final Object image) {} /** * Releases an image. * * @param image the image */ public void unregisterImage(final Object image) {} /** * Logs a debug message (to be shown only in debug mode). * * @param string the debug message */ public void debug(final String string) { System.err.println(string); } /** * Shows an exception. * * @param t the exception */ public void error(final Throwable t) { // ignore } /** * Returns the name to use in place of "ImageJ". * * @return the application name */ public String getAppName() { return "ImageJ"; } /** * Returns the version to use in place of the legacy version. * * @return the application version, or null if we do not override */ public String getAppVersion() { return null; } /** * Returns the icon to use in place of the ImageJ microscope. * * @return the URL to the icon to use, or null */ public URL getIconURL() { return null; } /** * Extension point to override ImageJ 1.x' editor. * * @param path the path to the file to open * @return true if the hook opened a different editor */ public boolean openInEditor(final String path) { return false; } /** * Extension point to override ImageJ 1.x' editor. * * @param fileName the name of the new file * @param content the initial content * @return true if the hook opened a different editor */ public boolean createInEditor(final String fileName, final String content) { return false; } private boolean enableIJ1PluginDirs = true; protected void enableIJ1PluginDirs(final boolean enable) { enableIJ1PluginDirs = enable; } final private Collection pluginClasspath = new LinkedHashSet(); protected void addPluginClasspath(final File file) { pluginClasspath.add(file); } /** * Extension point to add to ImageJ 1.x' PluginClassLoader's class path. * * @return a list of class path elements to add */ public List handleExtraPluginJars() { final List result = new ArrayList(); result.addAll(pluginClasspath); if (!enableIJ1PluginDirs) return result; final String extraPluginDirs = System.getProperty("ij1.plugin.dirs"); if (extraPluginDirs != null) { for (final String dir : extraPluginDirs.split(File.pathSeparator)) { final File directory = new File(dir); if (directory.isDirectory()) { result.add(directory); handleExtraPluginJars(directory, result); } } return result; } final String userHome = System.getProperty("user.home"); if (userHome != null) { final File dir = new File(userHome, ".plugins"); if (dir.isDirectory()) { result.add(dir); handleExtraPluginJars(dir, result); } } return result; } private void handleExtraPluginJars(final File directory, final List result) { final File[] list = directory.listFiles(); if (list == null) return; for (final File file : list) { if (file.isDirectory()) handleExtraPluginJars(file, result); else if (file.isFile() && file.getName().endsWith(".jar")) { result.add(file); } } } /** * Extension point to run after Help>Refresh Menus */ public void runAfterRefreshMenus() { // ignore } /** * Extension point to enhance ImageJ 1.x' error reporting upon * {@link NoSuchMethodError}. * * @param error the exception to handle * @return true if the error was handled by the legacy hook */ public boolean handleNoSuchMethodError(final NoSuchMethodError error) { String message = error.getMessage(); int paren = message.indexOf("("); if (paren < 0) return false; int dot = message.lastIndexOf(".", paren); if (dot < 0) return false; String path = message.substring(0, dot).replace('.', '/') + ".class"; Set urls = new LinkedHashSet(); final ClassLoader loader = Thread.currentThread().getContextClassLoader(); try { Enumeration e = loader.getResources(path); while (e.hasMoreElements()) { urls.add(e.nextElement().toString()); } e = loader.getResources("/" + path); while (e.hasMoreElements()) { urls.add(e.nextElement().toString()); } } catch (Throwable t) { t.printStackTrace(); return false; } if (urls.size() == 0) return false; StringBuilder buffer = new StringBuilder(); buffer.append("There was a problem with the class "); buffer.append(message.substring(0, dot)); buffer.append(" which can be found here:\n"); for (String url : urls) { if (url.startsWith("jar:")) url = url.substring(4); if (url.startsWith("file:")) url = url.substring(5); int bang = url.indexOf("!"); if (bang < 0) buffer.append(url); else buffer.append(url.substring(0, bang)); buffer.append("\n"); } if (urls.size() > 1) { buffer.append("\nWARNING: multiple locations found!\n"); } StringWriter writer = new StringWriter(); error.printStackTrace(new PrintWriter(writer)); buffer.append(writer.toString()); System.out.println(buffer.toString()); final NoSuchMethodException throwable = new NoSuchMethodException("Could not find method " + message + "\n" + buffer); throwable.setStackTrace(error.getStackTrace()); error(throwable); return true; } /** * Extension point to run after a new PluginClassLoader was initialized. * * @param loader the PluginClassLoader instance */ public void newPluginClassLoader(final ClassLoader loader) { // do nothing } /** * Extension point to modify the order in which .jar files are added to the * PluginClassLoader. *

* There is a problem which only strikes large distributions of ImageJ such as * Fiji: some .jar files try to be helpful and bundle classes which are * actually not theirs, causing problems when newer versions of those .jar * files which they shadow are present in the plugins/ or jars/ * directory but are not respected by the class loader. *

*

* The default hook of this extension point therefore hard-codes a few file * names of known offenders (which we politely will call fat .jar files * normally) and just pushes them back to the end of the list. *

* * @param directory the directory which ImageJ 1.x looked at * @param names the list of file names in the order ImageJ 1.x discovered them * @return the ordered, filtered and/or augmented list */ public String[] addPluginDirectory(final File directory, final String[] names) { if (names != null) { /* Note that this code is replicated in imagej-launcher's ClassLoaderPlus class. Improvements to this Pattern string should also be mirrored there. */ final Pattern pattern = Pattern.compile("(batik|jython|jython-standalone|jruby)(-[0-9].*)?\\.jar"); Arrays.sort(names, new FatJarNameComparator(pattern)); } return names; } /** * Comparator to ensure that problematic fat JARs are sorted last. * It is intended to be used with a {@link Pattern} that filters things this * way. */ public final static class FatJarNameComparator implements Comparator { private final Pattern pattern; private FatJarNameComparator(Pattern pattern) { this.pattern = pattern; } @Override public int compare(final String a, final String b) { return (pattern.matcher(a).matches() ? 1 : 0) - (pattern.matcher(b).matches() ? 1 : 0); } } /** * First extension point to run just after ImageJ 1.x spun up. */ public void initialized() { // do nothing by default } public InputStream autoGenerateConfigFile(final File directory) { // skip unpacked ImageJ 1.x if (new File(directory, "IJ_Props.txt").exists()) return null; return new ByteArrayInputStream(autoGenerateConfigFile(directory, directory, "Plugins", "", new StringBuilder()).toString().getBytes()); } protected StringBuilder autoGenerateConfigFile(final File topLevelDirectory, final File directory, final String menuPath, final String packageName, final StringBuilder builder) { final File[] list = directory.listFiles(); if (list == null) return builder; // make order consistent Arrays.sort(list); for (final File file : list) { String name = file.getName(); if (name.startsWith("_")) continue; if (file.isDirectory()) { autoGenerateConfigFile(topLevelDirectory, file, menuPath + ">" + name.replace('_', ' '), packageName + name + ".", builder); } else if (name.endsWith(".class") && name.contains("_") && !name.contains("$")) { if (topLevelDirectory == directory && Character.isLowerCase(name.charAt(0))) continue; final String className = packageName + name.substring(0, name.length() - 6); name = name.substring(0, name.length() - 6).replace('_', ' '); builder.append(menuPath + ", \"" + name + "\", " + className + "\n"); } } return builder; } private Map menuStructure = new LinkedHashMap(); /** * Callback for ImageJ 1.x' menu parsing machinery. *

* This method is called whenever ImageJ 1.x adds a command to the menu structure. *

* * @param menuPath the menu path of the menu item, or null when reinitializing * @param command the command associated with the menu item, or null when reinitializing */ public void addMenuItem(final String menuPath, final String command) { if (menuPath == null) { menuStructure.clear(); } else if (menuPath.startsWith("Help>Examples>")) { // NB: Skip the Help>Examples menu. We do not need it in headless mode. return; } else if (menuPath.endsWith(">-")) { int i = 1; while (menuStructure.containsKey(menuPath + i)) { i++; } menuStructure.put(menuPath + i, command); } else { menuStructure.put(menuPath, command); } } /** * Returns ImageJ 1.x' menu structure as a map. * * @return the menu structure */ public Map getMenuStructure() { return Collections.unmodifiableMap(menuStructure); } /** * Optionally override opening resources via legacy hooks. *

* This is intended as a "HandleExtraFileTypesPlus". *

* * @param path the path to the resource to open, or {@code null} if a dialog * needs to be shown * @param planeIndex * If applicable - the index of plane to open or -1 for all planes * @param display * if true, the opened object should be displayed before returning * @return The opened object, or {@code null} to let ImageJ 1.x open the path. * @deprecated this will be removed before ij1-patcher 1.0.0 */ @Deprecated public Object interceptOpen(final String path, final int planeIndex, final boolean display) { return null; } /** * Optionally override opening resources via legacy hooks. *

* This is intended as a "HandleExtraFileTypesPlus". *

* * @param path the path to the resource to open, or {@code null} if a dialog * needs to be shown * @return The opened object, or {@code null} to let ImageJ 1.x open the resource. */ public Object interceptFileOpen(final String path) { return null; } /** * Optionally override opening images via legacy hooks. *

* This is intended as a "HandleExtraFileTypesPlus". *

* * @param path the path to the image to open, or {@code null} if a dialog * needs to be shown * @param planeIndex * If applicable - the index of plane to open or -1 for all planes * @return The opened image, or {@code null} to let ImageJ 1.x open the image. */ public Object interceptOpenImage(final String path, final int planeIndex) { return null; } /** * Optionally override opening recent images via legacy hooks. * * @param path the path to the recent image to open * @return The opened object, or {@code null} to let ImageJ 1.x open the image. */ public Object interceptOpenRecent(final String path) { return null; } /** * Optionally override opening drag-and-dropped files via legacy hooks. * * @param f the file that was dragged onto the IJ UI * @return The opened object, or {@code null} to let ImageJ 1.x open the file * as normal. */ public Object interceptDragAndDropFile(final File f) { return null; } /** * Do not use: for internal use only. */ public static Collection getClasspathElements( final ClassLoader fromClassLoader, final StringBuilder errors, final ClassLoader... excludeClassLoaders) { final Set exclude = new HashSet(Arrays.asList(excludeClassLoaders)); final List result = new ArrayList(); for (ClassLoader loader = fromClassLoader; loader != null; loader = loader.getParent()) { if (exclude.contains(loader)) break; if (!(loader instanceof URLClassLoader)) { errors.append("Cannot add class path from ClassLoader of type ") .append(fromClassLoader.getClass().getName()).append("\n"); continue; } for (final URL url : ((URLClassLoader) loader).getURLs()) { if (!"file".equals(url.getProtocol())) { errors.append("Not a file URL! ").append(url).append("\n"); continue; } result.add(new File(url.getPath())); final String path = url.getPath(); if (path.matches(".*/target/surefire/surefirebooter[0-9]*\\.jar")) try { final JarFile jar = new JarFile(path); final Manifest manifest = jar.getManifest(); if (manifest != null) { final String classPath = manifest.getMainAttributes().getValue(Name.CLASS_PATH); if (classPath != null) { for (final String element : classPath.split(" +")) try { final URL url2 = new URL(element); if (!"file".equals(url2.getProtocol())) { errors.append("Not a file URL! ").append(url2).append("\n"); continue; } result.add(new File(url2.getPath())); } catch (final MalformedURLException e) { e.printStackTrace(); } } } } catch (final IOException e) { System.err .println("Warning: could not add plugin class path due to "); e.printStackTrace(); } } } return result; } /** * Intercepts keyboard events sent to ImageJ 1.x. * * @param e the keyboard event * @return whether the event was intercepted */ public boolean interceptKeyPressed(final KeyEvent e) { return false; } /** * Iterates through the current thread's ancestors. *

* ImageJ 1.x' macro options are thread-local. Unfortunately, this does not * take into account thread relationships e.g. when threads are spawned in * parallel. *

*

* By overriding this method, legacy hooks can force ImageJ 1.x to look harder * for macro options. *

* * @return the ancestor(s) of the current thread, or null */ public Iterable getThreadAncestors() { return null; } /** * Allows closing additional windows at the end of * {@link ij.WindowManager#closeAllWindows()}. *

* When returning {@code false}, ImageJ 1.x will be disallowed from quitting. *

* * @return whether it is okay to quit */ public boolean interceptCloseAllWindows() { return true; } /** * Hook to ensure {@link ij.gui.ImageWindow}s are fully cleaned up when * they are closed. */ public void interceptImageWindowClose(final Object window) { // nothing to do } /** * Allows interception of ImageJ 1.x's disposal routine while quitting. *

* This method is called after it has been confirmed that quitting should * proceed. That is, the user OKed all the windows being closed, etc. * This method provides one final chance to cancel the quit operation by * returning false; otherwise, it performs any needed disposal and cleanup. *

* * @return whether ImageJ 1.x should proceed in quitting * @see ij.ImageJ#run() which is where ImageJ 1.x actually quits */ public boolean disposing() { return true; } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy