/* * Open Source Physics software is free software as described near the bottom of this code file. * * For additional information and documentation on Open Source Physics please see: * <http://www.opensourcephysics.org/> */ package org.opensourcephysics.tools; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Component; import java.awt.Container; import java.awt.Dimension; import java.awt.Toolkit; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.io.File; import java.net.JarURLConnection; import java.net.MalformedURLException; import java.net.URL; import java.net.URLClassLoader; import java.util.ArrayList; import java.util.Collection; import java.util.Enumeration; import java.util.Iterator; import java.util.Map; import java.util.TreeMap; import java.util.jar.JarEntry; import java.util.jar.JarFile; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.swing.BorderFactory; import javax.swing.Box; import javax.swing.BoxLayout; import javax.swing.JApplet; import javax.swing.JButton; import javax.swing.JComponent; import javax.swing.JDialog; import javax.swing.JLabel; import javax.swing.JList; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JTextField; import javax.swing.ListSelectionModel; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; import org.opensourcephysics.controls.OSPLog; import org.opensourcephysics.controls.XML; import org.opensourcephysics.display.OSPRuntime; /** * This modal dialog lets the user choose launchable classes from jar files. */ public class LaunchClassChooser extends JDialog { // static fields private static Pattern pattern; private static Matcher matcher; private static Map<String, LaunchableClassMap> classMaps = new TreeMap<String, LaunchableClassMap>(); // maps path to classMap protected static boolean jarsOnly = true; protected static String baseDirectoryPath; // instance fields private JTextField searchField; private String defaultSearch = ""; //$NON-NLS-1$ private String currentSearch = defaultSearch; private JScrollPane scroller; private JList choices; // list of search results (class names) private LaunchableClassMap classMap; // map of available launchable classes private boolean applyChanges = false; private JButton okButton; /** * Constructs an empty LaunchClassChooser dialog. * * @param owner the component that owns the dialog (may be null) */ public LaunchClassChooser(Component owner) { super(JOptionPane.getFrameForComponent(owner), true); setTitle(LaunchRes.getString("ClassChooser.Frame.Title")); //$NON-NLS-1$ JLabel textLabel = new JLabel(LaunchRes.getString("ClassChooser.Search.Label")+" "); //$NON-NLS-1$ //$NON-NLS-2$ // create the buttons okButton = new JButton(LaunchRes.getString("ClassChooser.Button.Accept")); //$NON-NLS-1$ okButton.setEnabled(false); okButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { applyChanges = true; setVisible(false); } }); JButton cancelButton = new JButton(LaunchRes.getString("ClassChooser.Button.Cancel")); //$NON-NLS-1$ cancelButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { setVisible(false); } }); // create the search field searchField = new JTextField(defaultSearch); searchField.addKeyListener(new KeyAdapter() { public void keyReleased(KeyEvent e) { Object obj = choices.getSelectedValue(); if("model".equals(searchField.getName())) { //$NON-NLS-1$ searchForModel(); } else { search(); } choices.setSelectedValue(obj, true); } }); getRootPane().setDefaultButton(okButton); // lay out the header pane JPanel headerPane = new JPanel(); headerPane.setLayout(new BoxLayout(headerPane, BoxLayout.X_AXIS)); headerPane.add(textLabel); headerPane.add(Box.createHorizontalGlue()); headerPane.add(searchField); headerPane.setBorder(BorderFactory.createEmptyBorder(10, 10, 0, 10)); // lay out the scroll pane JPanel scrollPane = new JPanel(new BorderLayout()); scrollPane.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); // lay out the button pane JPanel buttonPane = new JPanel(); buttonPane.setLayout(new BoxLayout(buttonPane, BoxLayout.X_AXIS)); buttonPane.setBorder(BorderFactory.createEmptyBorder(0, 10, 10, 10)); buttonPane.add(Box.createHorizontalGlue()); buttonPane.add(okButton); buttonPane.add(Box.createRigidArea(new Dimension(10, 0))); buttonPane.add(cancelButton); // add everything to the content pane Container contentPane = getContentPane(); contentPane.add(headerPane, BorderLayout.NORTH); contentPane.add(scrollPane, BorderLayout.CENTER); contentPane.add(buttonPane, BorderLayout.SOUTH); // create the scroll pane scroller = new JScrollPane(); scroller.setPreferredSize(new Dimension(400, 300)); scrollPane.add(scroller, BorderLayout.CENTER); pack(); // center dialog on the screen Dimension dim = Toolkit.getDefaultToolkit().getScreenSize(); int x = (dim.width-this.getBounds().width)/2; int y = (dim.height-this.getBounds().height)/2; setLocation(x, y); } /** * Sets the path to be searched. The path must be a set of jar file names * separated by semicolons unless jarsOnly is set to false. * * @param path the search path * @return true if at least one jar file was successfully loaded */ public boolean setPath(String path) { String[] jarNames = parsePath(path); // reset class map to null classMap = null; if((jarNames==null)||(jarNames.length==0)) { return false; } // create classMaps key String key = ""; //$NON-NLS-1$ for(int i = 0; i<jarNames.length; i++) { if(!key.equals("")) { //$NON-NLS-1$ // separate jars with semicolon key += ";"; //$NON-NLS-1$ } key += jarNames[i]; } // set the current classMap classMap = classMaps.get(key); if(classMap==null) { classMap = new LaunchableClassMap(jarNames); classMaps.put(key, classMap); } return true; } /** * Determines if the specified path is loaded. This will return true * only if the path is one or more jar files all of which are loaded. * * @param path the path * @return true if all jars in the path are loaded */ public boolean isLoaded(String path) { if(classMap==null) { return false; } String[] jarNames = parsePath(path); for(int i = 0; i<jarNames.length; i++) { if(!classMap.includesJar(jarNames[i])) { return false; } } return true; } /** * Chooses a launchable class and assigns it to the specified launch node. * * @param node the node * @return true if the class assignment is approved */ public boolean chooseClassFor(LaunchNode node) { if("model".equals(searchField.getName())) { //$NON-NLS-1$ searchField.setText(null); } setTitle(LaunchRes.getString("ClassChooser.Frame.Title")); //$NON-NLS-1$ searchField.setName("launch"); //$NON-NLS-1$ search(); // select node's current launch class, if any choices.setSelectedValue(node.launchClassName, true); applyChanges = false; setVisible(true); if(!applyChanges) { return false; } Object obj = choices.getSelectedValue(); if(obj==null) { return false; } String className = obj.toString(); // get the class name and the launchable class node.launchClass = classMap.get(className); node.launchClassName = className; node.launchModelScroller = null; return true; } /** * Chooses and returns a model class. * * @param previousClassName the previous class name. May be null. * @return the newly selected class. May return null. */ public Class<?> chooseModel(String previousClassName) { if("launch".equals(searchField.getName())) { //$NON-NLS-1$ searchField.setText(null); } setTitle(LaunchRes.getString("ModelClassChooser.Frame.Title")); //$NON-NLS-1$ searchField.setName("model"); //$NON-NLS-1$ searchForModel(); // select tab's previous model class, if any choices.setSelectedValue(previousClassName, true); applyChanges = false; setVisible(true); if(!applyChanges) { return null; } Object obj = choices.getSelectedValue(); if(obj==null) { return null; } String className = obj.toString(); return classMap.models.get(className); } /** * Gets the class with the given name in the current class map. * * @param className the class name * @return the Class object, or null if not found */ public Class<?> getClass(String className) { if(classMap==null) { return null; } return classMap.getClass(className); } /** * Sets the base directory for classpaths. * * @param path the base directory path */ public static void setBasePath(String path) { baseDirectoryPath = path; } /** * Gets the class with the given name in the specified path. * * @param classPath the path * @param className the class name * @return the Class object, or null if not found */ public static Class<?> getClass(String classPath, String className) { if((classPath==null)||(className==null)) { return null; } // get the classMap for the specified path String[] jarNames = parsePath(classPath); LaunchableClassMap classMap = getClassMap(jarNames); // get the class from the classMap return classMap.getClass(className); } /** * Gets the modelPane class with the given name in the specified path. * * @param classPath the path * @param className the class name * @return the Class object, or null if not found */ public static Class<?> getModelClass(String classPath, String className) { if((classPath==null)||(className==null)) { return null; } // get the classMap for the specified path String[] jarNames = parsePath(classPath); LaunchableClassMap classMap = getClassMap(jarNames); // get the class from the classMap return classMap.getModelClass(className); } /** * Gets the class with the given name in the specified path. * * @param classPath the path * @param className the class name * @param type a subclass of the desired class * @return the Class object, or null if not found */ public static Class<?> getClassOfType(String classPath, String className, Class<?> type) { if((classPath==null)||(className==null)) { return null; } // get the classMap for the specified path String[] jarNames = parsePath(classPath); LaunchableClassMap classMap = getClassMap(jarNames); // get the class from the classMap return classMap.getClassOfType(className, type); } /** * Gets the ClassLoader for the specified path. * * @param classPath the path * @return the ClassLoader object, or null if not found */ public static ClassLoader getClassLoader(String classPath) { if((classPath==null)||classPath.equals("")) { //$NON-NLS-1$ return null; } // get the classMap for the specified path String[] jarNames = parsePath(classPath); LaunchableClassMap classMap = getClassMap(jarNames); // get the class loader from the classMap return classMap.classLoader; } /** * Gets the launchable class map for the specified jar name array. * * @param jarNames the string array of jar names * @return the class map */ private static LaunchableClassMap getClassMap(String[] jarNames) { // create a key string from the jar names String key = ""; //$NON-NLS-1$ for(int i = 0; i<jarNames.length; i++) { if(!key.equals("")) { //$NON-NLS-1$ key += ";"; //$NON-NLS-1$ } key += jarNames[i]; } // get the classMap for the key LaunchableClassMap classMap = classMaps.get(key); if(classMap==null) { classMap = new LaunchableClassMap(jarNames); classMaps.put(key, classMap); } return classMap; } /** * Searches using the current search field text. */ private void search() { if(classMap==null) { return; } classMap.loadAllClasses(); if(search(searchField.getText())) { currentSearch = searchField.getText(); searchField.setBackground(Color.white); } else { JOptionPane.showMessageDialog(this, LaunchRes.getString("Dialog.InvalidRegex.Message")+" \""+searchField.getText()+"\"", LaunchRes.getString("Dialog.InvalidRegex.Title"), JOptionPane.WARNING_MESSAGE); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ searchField.setText(currentSearch); } } /** * Searches for a modelPane using the current search field text. */ private void searchForModel() { if(classMap==null) { return; } classMap.loadAllClasses(); if(searchForModel(searchField.getText())) { currentSearch = searchField.getText(); searchField.setBackground(Color.white); } else { JOptionPane.showMessageDialog(this, LaunchRes.getString("Dialog.InvalidRegex.Message")+" \""+searchField.getText()+"\"", LaunchRes.getString("Dialog.InvalidRegex.Title"), JOptionPane.WARNING_MESSAGE); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ searchField.setText(currentSearch); } } /** * Searches for class names using a regular expression string * and puts matches into the class chooser list of choices. * * @param regex the regular expression * @return true if the search succeeded (even if no matches found) */ private boolean search(String regex) { regex = regex.toLowerCase(); okButton.setEnabled(false); try { pattern = Pattern.compile(regex); } catch(Exception ex) { return false; } ArrayList<String> matches = new ArrayList<String>(); for(Iterator<?> it = classMap.keySet().iterator(); it.hasNext(); ) { String name = (String) it.next(); matcher = pattern.matcher(name.toLowerCase()); if(matcher.find()) { matches.add(name); } } Object[] results = matches.toArray(); choices = new JList(results); choices.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); choices.setFont(searchField.getFont()); choices.addListSelectionListener(new ListSelectionListener() { public void valueChanged(ListSelectionEvent e) { JList theList = (JList) e.getSource(); okButton.setEnabled(!theList.isSelectionEmpty()); } }); choices.addMouseListener(new MouseAdapter() { public void mousePressed(MouseEvent e) { JList theList = (JList) e.getSource(); if((e.getClickCount()==2)&&!theList.isSelectionEmpty()) { okButton.doClick(); } } }); scroller.getViewport().setView(choices); return true; } /** * Searches for class names using a regular expression string * and puts matches into the class chooser list of choices. * * @param regex the regular expression * @return true if the search succeeded (even if no matches found) */ private boolean searchForModel(String regex) { regex = regex.toLowerCase(); okButton.setEnabled(false); try { pattern = Pattern.compile(regex); } catch(Exception ex) { return false; } ArrayList<String> matches = new ArrayList<String>(); for(Iterator<?> it = classMap.models.keySet().iterator(); it.hasNext(); ) { String name = (String) it.next(); matcher = pattern.matcher(name.toLowerCase()); if(matcher.find()) { matches.add(name); } } Object[] results = matches.toArray(); choices = new JList(results); choices.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); choices.setFont(searchField.getFont()); choices.addListSelectionListener(new ListSelectionListener() { public void valueChanged(ListSelectionEvent e) { JList theList = (JList) e.getSource(); okButton.setEnabled(!theList.isSelectionEmpty()); } }); choices.addMouseListener(new MouseAdapter() { public void mousePressed(MouseEvent e) { JList theList = (JList) e.getSource(); if((e.getClickCount()==2)&&!theList.isSelectionEmpty()) { okButton.doClick(); } } }); scroller.getViewport().setView(choices); return true; } /** * Parses the specified path into path tokens (at semicolons). * * @param path the path * @param jarsOnly true if only ".jar" names are returned * @return an array of path names */ static String[] parsePath(String path) { return parsePath(path, jarsOnly); } /** * Parses the specified path into path tokens (at semicolons). * * @param path the path * @param jarsOnly true if only ".jar" names are returned * @return an array of path names */ static String[] parsePath(String path, boolean jarsOnly) { Collection<String> tokens = new ArrayList<String>(); // get the first path token String next = path; int i = path.indexOf(";"); //$NON-NLS-1$ if(i!=-1) { next = path.substring(0, i); path = path.substring(i+1); } else { path = ""; //$NON-NLS-1$ } // iterate thru the path tokens, trim and add to token list while(next.length()>0) { if(!jarsOnly||next.endsWith(".jar")) { //$NON-NLS-1$ tokens.add(next); } i = path.indexOf(";"); //$NON-NLS-1$ if(i==-1) { next = path.trim(); path = ""; //$NON-NLS-1$ } else { next = path.substring(0, i).trim(); path = path.substring(i+1).trim(); } } return tokens.toArray(new String[0]); } } /** * A map of class name to launchable Class object. * The name of the jar or directory is prepended to the class name. */ class LaunchableClassMap extends TreeMap<String, Class<?>> { // instance fields ClassLoader classLoader; // loads classes String[] jarOrDirectoryNames; // paths of jars or directories relative to jar base boolean allLoaded = false; TreeMap<String, Class<?>> models = new TreeMap<String, Class<?>>(); // constructor creates class loader LaunchableClassMap(String[] names) { jarOrDirectoryNames = names; // create a URL for each jar or directory Collection<URL> urls = new ArrayList<URL>(); // changed by D Brown 2007-11-06 for Linux // changed by F Esquembre and D Brown 2010-03-02 to allow arbitrary base path String basePath = LaunchClassChooser.baseDirectoryPath; if (basePath==null) basePath = OSPRuntime.getLaunchJarDirectory(); for(int i = 0; i<names.length; i++) { String path = XML.getResolvedPath(names[i], basePath); if (!path.endsWith(".jar") && !path.endsWith("/")) { //$NON-NLS-1$ //$NON-NLS-2$ path += "/"; // directories passed to URLClassLoader must end with slash //$NON-NLS-1$ } try { urls.add(new URL("file:"+path)); //$NON-NLS-1$ } catch(MalformedURLException ex) { OSPLog.info(ex+" "+path); //$NON-NLS-1$ } } // create the class loader classLoader = URLClassLoader.newInstance(urls.toArray(new URL[0])); } /** * Loads a class from the URLClassLoader or, if this fails, from the * current class loader. * * @param name the class name * @return the Class * @throws ClassNotFoundException */ Class<?> smartLoadClass(String name) throws ClassNotFoundException { try { return classLoader.loadClass(name); } catch(ClassNotFoundException e) { // added by Kip Barros return this.getClass().getClassLoader().loadClass(name); } } // returns a list of class files at all depths in a specified directory ArrayList<File> getClassFiles(File directory) { ArrayList<File> files = new ArrayList<File>(); for (File next: directory.listFiles()) { if (next.isDirectory()) { files.addAll(getClassFiles(next)); } else if (next.getName().endsWith(".class")) { //$NON-NLS-1$ files.add(next); } } return files; } // loads all launchable and model classes void loadAllClasses() { if(allLoaded) { return; } JApplet applet = org.opensourcephysics.display.OSPRuntime.applet; // for each jar or directory name, find launchable classes for(String next: jarOrDirectoryNames) { if (next.indexOf(".jar")>-1) { // next is a relative jar path //$NON-NLS-1$ // create a JarFile JarFile jar = null; try { if(applet==null) { // application mode String basePath = LaunchClassChooser.baseDirectoryPath; if (basePath==null) basePath = OSPRuntime.getLaunchJarDirectory(); String path = XML.getResolvedPath(next, basePath); jar = new JarFile(path); } else { // applet mode String path = XML.getResolvedPath(next, applet.getCodeBase().toExternalForm()); // create a URL that refers to a jar file on the web URL url = new URL("jar:"+path+"!/"); //$NON-NLS-1$ //$NON-NLS-2$ // get the jar JarURLConnection conn = (JarURLConnection) url.openConnection(); jar = conn.getJarFile(); } } catch(Exception ex) { OSPLog.info(ex.getClass().getName()+": "+ex.getMessage()); //$NON-NLS-1$ } if(jar==null) { continue; } // iterate thru JarFile entries and load classes for(Enumeration<JarEntry> e = jar.entries(); e.hasMoreElements(); ) { JarEntry entry = e.nextElement(); String name = entry.getName(); if(name.endsWith(".class")) { //$NON-NLS-1$ loadClass(name); } } } else { // next is a relative directory path String basePath = LaunchClassChooser.baseDirectoryPath; if (basePath==null) basePath = OSPRuntime.getLaunchJarDirectory(); String directoryPath = XML.getResolvedPath(next, basePath); for (File nextFile: getClassFiles(new File(directoryPath))) { String name = XML.getPathRelativeTo(nextFile.getPath(), directoryPath); loadClass(name); } } } allLoaded = true; } // load a class and, if launchable or a model, store it with class name key void loadClass(String name) { // ignore manifest and inner classes if(name.indexOf("$")==-1) { //$NON-NLS-1$ // remove class extension and replace slashes name = name.substring(0, name.indexOf(".class")); //$NON-NLS-1$ int j = name.indexOf("/"); //$NON-NLS-1$ while(j!=-1) { name = name.substring(0, j)+"."+name.substring(j+1); //$NON-NLS-1$ j = name.indexOf("/"); //$NON-NLS-1$ } // return if class is already loaded if((get(name)!=null)||(models.get(name)!=null)) { return; } try { // load the class Class<?> nextClass = smartLoadClass(name); // changed by Kip Barros // if launchable, store it if(Launcher.isLaunchable(nextClass)) { put(name, nextClass); } // if a model, store it if(Launcher.isModel(nextClass)) { models.put(name, nextClass); } } catch(ClassNotFoundException ex) { /** empty block */ } catch(NoClassDefFoundError err) { OSPLog.info(err.toString()); } } } // returns true of this classMap includes specified jar boolean includesJar(String jarName) { for(int i = 0; i<jarOrDirectoryNames.length; i++) { if(jarOrDirectoryNames[i].equals(jarName)) { return true; } } return false; } // gets the specified class, or null if not loadable or launchable Class<?> getClass(String className) { Class<?> type = get(className); if((type!=null)||allLoaded) { return type; } try { // load the class and, if launchable, return it type = smartLoadClass(className); // changed by Kip Barros if(Launcher.isLaunchable(type)) { return type; } } catch(ClassNotFoundException ex) { /** empty block */ } catch(NoClassDefFoundError err) { OSPLog.info(err.toString()); } return null; } // gets the specified model class, or null if not loadable or not a model Class<?> getModelClass(String className) { Class<?> type = models.get(className); if((type!=null)) { return type; } try { // load the class and, if a model, return it type = smartLoadClass(className); // changed by Kip Barros if(Launcher.isModel(type)) { return type; } if(JComponent.class.isAssignableFrom(type)) { try { type.getConstructor((Class[]) null); return type; } catch(Exception ex) {} } } catch(ClassNotFoundException ex) {} catch(NoClassDefFoundError err) { OSPLog.info(err.toString()); } return null; } // gets the specified class, or null if not found or wrong type Class<?> getClassOfType(String className, Class<?> type) { try { // load the class and, if right type, return it Class<?> theClass = smartLoadClass(className); if(type.isAssignableFrom(theClass)) { return theClass; } } catch(ClassNotFoundException ex) { /** empty block */ } catch(NoClassDefFoundError err) { OSPLog.info(err.toString()); } return null; } } /* * Open Source Physics software is free software; you can redistribute * it and/or modify it under the terms of the GNU General Public License (GPL) as * published by the Free Software Foundation; either version 2 of the License, * or(at your option) any later version. * Code that uses any portion of the code in the org.opensourcephysics package * or any subpackage (subdirectory) of this package must must also be be released * under the GNU GPL license. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston MA 02111-1307 USA * or view the license online at http://www.gnu.org/copyleft/gpl.html * * Copyright (c) 2007 The Open Source Physics project * http://www.opensourcephysics.org */