/* * Copyright (c) 2003-2012 Fred Hutchinson Cancer Research Center * * Licensed 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.fhcrc.cpl.toolbox.commandline; import org.fhcrc.cpl.toolbox.ApplicationContext; import org.apache.log4j.Logger; import java.util.*; import java.net.JarURLConnection; import java.net.URL; import java.net.URLDecoder; import java.io.File; import java.io.FileNotFoundException; import java.lang.reflect.Modifier; /** * Utility class for finding all command-line modules. * See comments on method discoverAllCommandLineModules() for important details. */ public class CommandLineModuleDiscoverer { protected static Logger _log = Logger.getLogger(CommandLineModuleDiscoverer.class); protected Map<String,CommandLineModule> allCommandLineModuleMap = null; protected static CommandLineModuleDiscoverer singletonInstance = null; public static CommandLineModuleDiscoverer getSingletonInstance() { if (singletonInstance == null) singletonInstance = new CommandLineModuleDiscoverer(); return singletonInstance; } //TODO: stick all this stuff into a resource file //packages in which all modules must reside public String[] modulePackageNames = new String[] { "org.fhcrc.cpl.toolbox.commandline", }; //an identifier string unique to this package, with only alphanumeric characters public String[] modulePackageIdentifiers = new String[] { "general", }; //Short (1-2 words) user-readable descriptions of each commandline module package -- what's it for? public String[] modulePackageDescriptionsShort = new String[] { "General", }; //Longer (1-2 sentences) user-readable descriptions of each commandline module package -- what's it for? public String[] modulePackageDescriptionsLong = new String[] { "General Command Modules", }; /** * Tell this CommandLineModuleDiscoverer where to look for packages and how to describe * what each package contains * @param modulePackageNames * @param modulePackageIdentifiers * @param modulePackageDescriptionsShort * @param modulePackageDescriptionsLong */ public void setPackageArrays(String[] modulePackageNames, String[] modulePackageIdentifiers, String[] modulePackageDescriptionsShort, String[] modulePackageDescriptionsLong) { this.modulePackageNames = modulePackageNames; this.modulePackageIdentifiers = modulePackageIdentifiers; this.modulePackageDescriptionsShort = modulePackageDescriptionsShort; this.modulePackageDescriptionsLong = modulePackageDescriptionsLong; } /** * Return all known commandline modules, initializing if necessary * @return */ public Map<String,CommandLineModule> findAllCommandLineModules() { if (allCommandLineModuleMap == null) allCommandLineModuleMap = discoverAllCommandLineModules(); return allCommandLineModuleMap; } /** * Broken up by package. Probably it would be better to store this structure rather than the simple map. * Would take some reorganization * @return */ public Map<String, Map<String, CommandLineModule>> findAllCommandLineModulesByPackage() { Map<String, Map<String, CommandLineModule>> result = new HashMap<String, Map<String, CommandLineModule>>(); Map<String,CommandLineModule> commandLineModuleMap = null; commandLineModuleMap = findAllCommandLineModules(); for (String command : commandLineModuleMap.keySet()) { _log.debug("Finding module for command " + command); CommandLineModule module = commandLineModuleMap.get(command); //null-checking module package. This wouldn't be necessary if we weren't using one-jar Package modulePackage = module.getClass().getPackage(); String modulePackageName = (null == modulePackage? "general" : modulePackage.getName()); Map<String, CommandLineModule> packageModuleMap = result.get(modulePackageName); if (packageModuleMap == null) { packageModuleMap = new HashMap<String, CommandLineModule>(); result.put(modulePackageName, packageModuleMap); } packageModuleMap.put(command, module); } return result; } /** * This could be implemented much more efficiently * @param packageName * @return */ public String getPackageShortDescription(String packageName) { for (int i=0; i< modulePackageNames.length; i++) { if (packageName.equals(modulePackageNames[i])) return modulePackageDescriptionsShort[i]; } String result = packageName; if (packageName.contains(".")) result = packageName.substring(packageName.lastIndexOf(".")+1); return result; } /** * This could be implemented much more efficiently * @param packageName * @return */ public String getPackageIdentifier(String packageName) { for (int i=0; i< modulePackageNames.length; i++) { if (packageName.equals(modulePackageNames[i])) return modulePackageIdentifiers[i]; } String result = packageName; if (packageName.contains(".")) result = packageName.substring(packageName.lastIndexOf(".")+1); return result; } /** * This could be implemented much more efficiently * @param packageName * @return */ public String getPackageLongDescription(String packageName) { for (int i=0; i< modulePackageNames.length; i++) { if (packageName.equals(modulePackageNames[i])) return modulePackageDescriptionsLong[i]; } String result = packageName; if (packageName.contains(".")) result = packageName.substring(packageName.lastIndexOf(".")+1); return result; } /** * Attempts to discover all the classes in the package org.fhcrc.cpl.viewer.commandline.modules, * and other declared packages, * that implement the CommandLineModule interface, as determined by the context class loader * * * @return a map of CommandLineModule-implementing classes that exist within the correct package */ protected Map<String, CommandLineModule> discoverAllCommandLineModules() { HashMap<String, CommandLineModule> result = new HashMap<String, CommandLineModule>(); //This holds the list of discovered classes ArrayList<Class> classesInPackage = new ArrayList<Class>(); ClassLoader cld = Thread.currentThread().getContextClassLoader(); if (cld == null) { ApplicationContext.infoMessage("Failure finding commandline modules: couldn't get classloader"); return result; } //modules can be in any of the standard packages try { for (String modulePackageName : modulePackageNames) { //This will hold a list of directories matching the modulePackageName. There may be more than //one if a package is split over multiple jars/paths ArrayList<File> directories = new ArrayList<File>(); // this will hold a list of jars matching the pckgname. May be more than one ArrayList<URL> jarUrls = new ArrayList<URL>(); String path = modulePackageName.replace('.', '/'); // Ask for all resources for the path Enumeration<URL> resources = cld.getResources(path); while (resources.hasMoreElements()) { URL urlResource = resources.nextElement(); if (urlResource.getProtocol().equals("jar")) { jarUrls.add(urlResource); } else directories.add(new File(URLDecoder.decode(urlResource.getPath(), "UTF-8"))); } //First, add all the classes in the package to a list try { // For every directory identified capture all the matching .class files for (File directory : directories) { if (directory.exists()) { // Get the list of the files contained in the package String[] files = directory.list(); for (String file : files) { // we are only interested in .class files if (file.endsWith(".class") && !file.contains("$")) { // removes the .class extension classesInPackage.add(Class.forName(modulePackageName + '.' + file.substring(0, file.length() - ".class".length()))); } } } } // For every jar identified capture all the matching .class files for (URL jarUrl : jarUrls) { JarURLConnection conn = (JarURLConnection) jarUrl.openConnection(); java.util.jar.JarFile jfile = conn.getJarFile(); String starts = conn.getEntryName(); java.util.Enumeration e = jfile.entries(); while (e.hasMoreElements()) { java.util.zip.ZipEntry entry = (java.util.zip.ZipEntry) e.nextElement(); String entryname = entry.getName(); if (entryname.startsWith(starts) && (entryname.lastIndexOf('/') <= starts.length()) && entryname.endsWith(".class") && !entryname.contains("$")) { String classname = entryname.substring(0, entryname.length() - 6); if (classname.startsWith("/")) classname = classname.substring(1); classname = classname.replace('/', '.'); try { // Try to create an instance of the object classesInPackage.add(Class.forName(classname)); } catch (Throwable t) { } } } } } catch (Exception e) { ApplicationContext.errorMessage("Failure finding commandline modules for package " + modulePackageName + ", unable to search all directories", e); return result; } } } catch (Exception x) { ApplicationContext.errorMessage("Failure finding commandline modules", x); return result; } //Now, for each class identified, check to make sure it implements the CommandLineModule interface for (Class candidateClass : classesInPackage) { try { //don't try to instantiate abstract classes if (Modifier.isAbstract(candidateClass.getModifiers())) continue; Object candidateInstance = candidateClass.newInstance(); //only add the first discovered instance of a particular command if (candidateInstance instanceof CommandLineModule && !result.containsKey(((CommandLineModule) candidateInstance).getCommandName())) { result.put(((CommandLineModule) candidateInstance).getCommandName(), (CommandLineModule) candidateInstance); } } catch (InstantiationException ie) { _log.debug("WARNING: Found a class I couldn't instantiate in a commandline module package: " + candidateClass.getName()); } catch (Exception e) { ApplicationContext.errorMessage("Failure finding commandline module " + candidateClass.getName(), e); } } return result; } /** * Return the commandline module for the specified command. * If that module doesn't exist, throw a FileNotFoundExcption. * That's a bit of a hack, but it's necessary for an exception to be thrown * that's not a RunTimeException, to make the calling code handle it explicitly * * @param command * @return * @throws FileNotFoundException if no module exists for the specified command */ public CommandLineModule getCommandLineModule(String command) throws FileNotFoundException { CommandLineModule module = findAllCommandLineModules().get(command.toLowerCase()); if (module == null) throw new FileNotFoundException("No commandline Module found for command " + command); return module; } }