001/******************************************************************************* 002 * This software is provided as a supplement to the authors' textbooks on digital 003 * image processing published by Springer-Verlag in various languages and editions. 004 * Permission to use and distribute this software is granted under the BSD 2-Clause 005 * "Simplified" License (see http://opensource.org/licenses/BSD-2-Clause). 006 * Copyright (c) 2006-2023 Wilhelm Burger, Mark J. Burge. All rights reserved. 007 * Visit https://imagingbook.com for additional details. 008 ******************************************************************************/ 009package imagingbook.core.plugin; 010 011import ij.plugin.PlugIn; 012import ij.plugin.filter.PlugInFilter; 013import imagingbook.core.FileUtils; 014 015import java.io.File; 016import java.io.FileNotFoundException; 017import java.io.IOException; 018import java.io.PrintStream; 019import java.nio.file.Files; 020import java.nio.file.Path; 021import java.time.LocalDateTime; 022import java.time.format.DateTimeFormatter; 023import java.util.ArrayList; 024import java.util.List; 025import java.util.stream.Stream; 026 027/** 028 * <p> 029 * The {@code main()} method of this class creates the {@code plugins.config} file for a given plugins project, which is 030 * to be included in the associated JAR file. The execution is to be triggered during the Maven build or manually by 031 * </p> 032 * <pre> 033 * mvn exec:java -Dexec.mainClass="imagingbook.pluginutils.PluginsConfigBuilder"</pre> 034 * <p> 035 * (at the root of a plugins project). The format of the entries in {@code plugins.config} have the following 036 * structure: 037 * </p> 038 * <pre> 039 * menu-level, "plugin-name", package.classname</pre> 040 * for example: 041 * <pre> 042 * Plugins>Binary>Regions, "Convex Hull Demo", Binary_Regions.Convex_Hull_Demo</pre> 043 * <p>Note that, technically, menu paths may be more than 2 levels deep, but this 044 * does not seem useful. 045 * </p> 046 * <p> 047 * Plugin classes (implementing {@link PlugIn} or {@link PlugInFilter}) may be annotated with {@link IjPluginPath} and 048 * {@link IjPluginName} to specify where in ImageJ's menu tree and by which name the plugin should be installed. This 049 * information is stored in the {@code plugins.config} file at the root of the associated project, which is 050 * automatically added to the project's output JAR file during the Maven build. Example: 051 * </p> 052 * <pre> 053 * // file MySuperPlugin.java 054 * import ij.plugin.filter.PlugInFilter; 055 * import imagingbook.pluginutils.annotations.IjPluginName; 056 * import imagingbook.pluginutils.annotations.IjPluginPath; 057 * ... 058 * {@literal @}IjPluginPath("Plugins>Mine") 059 * {@literal @}IjPluginName("Super Plugin") 060 * public class MySuperPlugin implements PlugInFilter { 061 * // plugin code ... 062 * }</pre> 063 * <p> 064 * In this case, plugin {@code MySuperPlugin} should be installed in ImageJ's menu tree as 065 * <pre> Plugins > Mine > Super Plugin</pre> 066 * <p> 067 * By default (i.e., if no annotations are present), plugins in the default package are installed at the top-level of 068 * 'Plugins' whereas plugins inside a named package are installed in 'Plugins><em>package-name</em>' (see below). A 069 * {@link IjPluginPath} annotation may also be attached to a whole package in the associated {@code package-info.java} 070 * file. The following example specifies {@code Plugins>Binary Regions} as the default menu path for all plugins in 071 * package {@code Binary_Regions}: 072 * </p> 073 * <pre> 074 * // file Binary_Regions/package-info.java 075 * {@literal @}IjPluginPath("Plugins>Binary Regions") 076 * package Binary_Regions; 077 * import imagingbook.pluginutils.annotations.IjPluginPath;</pre> 078 * <p> 079 * Individual plugins may override the menu path specified for the containing package, as summarized below: 080 * <p> 081 * <strong>Plugin <em>path</em> priority rules summary:</strong> 082 * </p> 083 * <ol> 084 * <li> Value of a {@code @IjPluginPath} annotation at class level (always overrules if exists).</li> 085 * <li> Value of a {@code @IjPluginPath} annotation at package level (if exists).</li> 086 * <li> {@link #DefaultMenuPath} + {@literal ">"} + <em>package-name</em> if the plugin is inside a named package.</li> 087 * <li> {@link #DefaultMenuPath} if the plugin is in the (unnamed) default package.</li> 088 * </ol> 089 * <p> 090 * <strong>Plugin <em>entry</em> priority rules summary:</strong> 091 * </p> 092 * <ol> 093 * <li> Value of {@link IjPluginName} annotation at class level (if exists).</li> 094 * <li> Simple name of the plugin class.</li> 095 * </ol> 096 * <p> 097 * Note that, in general, ImageJ uses the information in file {@code plugins.config} 098 * only for plugins loaded from a JAR file! 099 * </p> 100 * 101 * @author WB 102 * @see IjPluginPath 103 * @see IjPluginName 104 */ 105public class PluginsConfigBuilder { 106 107 protected static String DefaultMenuPath = "Plugins"; // can be overridden by package or class annotation @IjPluginPath 108// static String DefaultEntryPrefix = "B&B "; 109 110 protected static String ConfigFileName = "plugins.config"; 111 protected static String INFO = "[INFO] "; 112 113 protected static boolean VERBOSE = true; 114 protected static boolean ReplaceUndescoresInClassNames = true; 115 protected static boolean ReplaceUndescoresInPackageNames = true; 116 117 private final String artifactId; 118 private final String rootPath; 119 120 /** 121 * Constructor (private), only called from the main() method. 122 * @param rootName the project's root (output) directory 123 * @param artifactId the project's Maven artifact id 124 */ 125 private PluginsConfigBuilder(String rootName, String artifactId) { 126 this.artifactId = artifactId; 127 File rootDir = (rootName != null) ? 128 new File(rootName) : 129 new File(PluginsConfigBuilder.class.getClassLoader().getResource("").getFile()); 130 this.rootPath = rootDir.getAbsolutePath(); 131 } 132 133 public List<Class<?>> collectPluginClasses(String rootPath) { 134 int n = rootPath.length(); 135 File rootFile = new File(rootPath); 136 if (!rootFile.exists()) { // this happens when executed in a project with POM-packaging 137 return null; 138 } 139 140 Path start = rootFile.toPath(); 141 List<Class<?>> pluginClasses = new ArrayList<>(); 142 143 try (final Stream<Path> allPaths = Files.walk(start)) { 144 allPaths.filter(Files::isRegularFile).forEach(path -> { 145 String pathName = path.toString(); 146 if (FileUtils.getFileExtension(pathName).equals("class")) { 147 String className = FileUtils.stripFileExtension(pathName); 148 // remove non-class part of filename: 149 className = className.substring(n + 1); 150 if (className.indexOf('-') < 0) { // ignore 'package-info' and 'module-info' 151 // convert to qualified class name: 152 className = className.replace(File.separatorChar, '.'); 153 // find the associated class object (this should never fail): 154 Class<?> clazz = null; 155 try { 156 clazz = Class.forName(className); 157 } catch (final ClassNotFoundException e) { 158 throw new RuntimeException(e.getMessage()); 159 } 160 161 if (clazz != null && isIjPlugin(clazz)) { 162 pluginClasses.add(clazz); 163 } 164 } 165 } 166 }); 167 } catch (IOException e) { 168 //throw new RuntimeException(e.getMessage()); 169 System.out.println("SOMETHING BAD HAPPENED: " + e.getMessage()); 170 } 171 return pluginClasses; 172 } 173 174 /** 175 * Writes plugin configuration entries to the specified stream. 176 * 177 * @param pluginClasses a list of plugin classes 178 * @param strm the output stream (e.g., System.out) 179 */ 180 private void writeEntriesToStream(List<Class<?>> pluginClasses, PrintStream strm) { 181 DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); 182 strm.println("# plugins.config file for " + artifactId + " (automatically generated)"); 183 strm.println("# number of plugins: " + pluginClasses.size()); 184 strm.println("# date: " + LocalDateTime.now().format(formatter)); 185 186 for (Class<?> clazz : pluginClasses) { 187 // configure menu path: 188 Package pkg = clazz.getPackage(); 189 String menuPath = DefaultMenuPath; 190 if (pkg != null) { 191 // see if 'package-info.java' contains specifies a menu path for this package 192 // TODO: warn if package nesting is deeper than 1 193 IjPluginPath packageMenuPathAnn = pkg.getDeclaredAnnotation(IjPluginPath.class); 194 String pkgName = (ReplaceUndescoresInPackageNames) ? 195 pkg.getName().replace('_', ' ') : pkg.getName(); 196 menuPath = (packageMenuPathAnn != null) ? 197 packageMenuPathAnn.value() : DefaultMenuPath + ">" + pkgName; 198 } 199 // see if clazz specifies a menu path for this package (overrules package specification) 200 IjPluginPath classMenuPathAnn = clazz.getDeclaredAnnotation(IjPluginPath.class); 201 if (classMenuPathAnn != null) { 202 menuPath = classMenuPathAnn.value(); 203 } 204 205 // configure menu entry: 206 IjPluginName classMenuEntryAnn = clazz.getDeclaredAnnotation(IjPluginName.class); 207 String className = (ReplaceUndescoresInClassNames) ? 208 clazz.getSimpleName().replace('_', ' ') : clazz.getSimpleName(); 209 String menuEntry = (classMenuEntryAnn != null) ? classMenuEntryAnn.value() : className; 210 // build line for entry in config file: 211 String configLine = String.format("%s, \"%s\", %s", menuPath, menuEntry, clazz.getCanonicalName()); 212 if (VERBOSE) 213 System.out.println(INFO + "*** " + configLine); 214 215// strm.format("%s, \"%s\", %s\n", menuPath, menuEntry, clazz.getCanonicalName()); 216 strm.println(configLine); 217 } 218 } 219 220 private String buildfile() { 221 List<Class<?>> pluginClasses = collectPluginClasses(rootPath); 222 if (pluginClasses == null) { 223 System.out.println(INFO + "WARNING: no target directory (POM project?)"); 224 return null; 225 } 226 if (pluginClasses.isEmpty()) { 227 System.out.println(INFO + "WARNING: no plugin classes found!"); 228 return null; 229 } 230 System.out.println(INFO + "Number of plugins: " + pluginClasses.size()); 231// writeEntriesToStream(pluginClasses, System.out); 232 233 File configFile = new File(rootPath + "/" + ConfigFileName); 234// System.out.println("configPath = " + configFile.getAbsolutePath()); 235 236 try (PrintStream ps = new PrintStream(configFile)) { 237 writeEntriesToStream(pluginClasses, ps); 238 } catch (FileNotFoundException e) { 239 throw new RuntimeException(e.getMessage()); 240 } 241 242 return configFile.getAbsolutePath(); 243 } 244 245 /** 246 * Returns true if the specified {@link Class} object is a sub-type of 247 * {@link PlugIn} or {@link PlugInFilter}. 248 * 249 * @param clazz a {@link Class} object 250 * @return true if a plugin type 251 */ 252 public boolean isIjPlugin(Class<?> clazz) { 253 return PlugIn.class.isAssignableFrom(clazz) || PlugInFilter.class.isAssignableFrom(clazz); 254 } 255 256 // ---------------------------------------------------------------------------------------------- 257 258 /** 259 * <p> 260 * Method to be called from the command line. Builds the {@code plugins.config} file 261 * from the {@code .class} files found in the specified build directory and 262 * stores the file in the same directory. 263 * Takes two (optional) arguments: 264 * </p> 265 * <ol> 266 * <li>The project's build (output) directory (where .class files reside).</li> 267 * <li> The project's Maven artifact id.</li> 268 * </ol> 269 * If no build directory is specified, the current directory is used. 270 * @param args {@code args[0]}: project build (output) directory, {@code args[1]}: project artefact id 271 */ 272 public static void main(String[] args) { 273 String rootName = (args.length > 0) ? args[0] : null; 274 String artifactId = (args.length > 1) ? args[1] : null; 275 System.out.println(INFO); 276 System.out.println(INFO + "--- Building plugins.config file for " + artifactId + " ---"); 277 for (String arg : args) { 278 System.out.println(INFO + " arg = |" + arg + "|"); 279 } 280 // future use to specify general plugins path from POM: 281 // using property <pluginPrefix>"Plugins>B&B "</pluginPrefix> 282 283 PluginsConfigBuilder builder = new PluginsConfigBuilder(rootName, artifactId); 284 String configPath = builder.buildfile(); 285 286 if (configPath != null) { 287 System.out.println(INFO + "Config file written: " + configPath); 288 } 289 else { 290 System.out.println(INFO + "No config file written"); 291 } 292 } 293 294}