1 /* 2 * Copyright (c) 2009-2010 jMonkeyEngine 3 * All rights reserved. 4 * 5 * Redistribution and use in source and binary forms, with or without 6 * modification, are permitted provided that the following conditions are 7 * met: 8 * 9 * * Redistributions of source code must retain the above copyright 10 * notice, this list of conditions and the following disclaimer. 11 * 12 * * Redistributions in binary form must reproduce the above copyright 13 * notice, this list of conditions and the following disclaimer in the 14 * documentation and/or other materials provided with the distribution. 15 * 16 * * Neither the name of 'jMonkeyEngine' nor the names of its contributors 17 * may be used to endorse or promote products derived from this software 18 * without specific prior written permission. 19 * 20 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 22 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 23 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR 24 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 25 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 26 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 27 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 28 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 29 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 30 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 */ 32 33 package jme3test; 34 35 import com.jme3.app.Application; 36 import com.jme3.app.SimpleApplication; 37 import com.jme3.system.JmeContext; 38 import java.awt.*; 39 import java.awt.event.*; 40 import java.io.File; 41 import java.io.FileFilter; 42 import java.io.IOException; 43 import java.io.UnsupportedEncodingException; 44 import java.lang.reflect.Field; 45 import java.lang.reflect.InvocationTargetException; 46 import java.lang.reflect.Method; 47 import java.net.JarURLConnection; 48 import java.net.URL; 49 import java.net.URLConnection; 50 import java.net.URLDecoder; 51 import java.util.Collection; 52 import java.util.Enumeration; 53 import java.util.Vector; 54 import java.util.jar.JarFile; 55 import java.util.logging.Level; 56 import java.util.logging.Logger; 57 import java.util.zip.ZipEntry; 58 import javax.swing.*; 59 import javax.swing.border.EmptyBorder; 60 import javax.swing.event.DocumentEvent; 61 import javax.swing.event.DocumentListener; 62 import javax.swing.event.ListSelectionEvent; 63 import javax.swing.event.ListSelectionListener; 64 65 66 /** 67 * Class with a main method that displays a dialog to choose any jME demo to be 68 * started. 69 */ 70 public class TestChooser extends JDialog { 71 private static final Logger logger = Logger.getLogger(TestChooser.class 72 .getName()); 73 74 private static final long serialVersionUID = 1L; 75 76 /** 77 * Only accessed from EDT 78 */ 79 private Object[] selectedClass = null; 80 private boolean showSetting = true; 81 82 /** 83 * Constructs a new TestChooser that is initially invisible. 84 */ TestChooser()85 public TestChooser() throws HeadlessException { 86 super((JFrame) null, "TestChooser"); 87 } 88 89 /** 90 * @param classes 91 * vector that receives the found classes 92 * @return classes vector, list of all the classes in a given package (must 93 * be found in classpath). 94 */ find(String pckgname, boolean recursive, Vector<Class> classes)95 protected Vector<Class> find(String pckgname, boolean recursive, 96 Vector<Class> classes) { 97 URL url; 98 99 // Translate the package name into an absolute path 100 String name = pckgname; 101 if (!name.startsWith("/")) { 102 name = "/" + name; 103 } 104 name = name.replace('.', '/'); 105 106 // Get a File object for the package 107 // URL url = UPBClassLoader.get().getResource(name); 108 url = this.getClass().getResource(name); 109 // URL url = ClassLoader.getSystemClassLoader().getResource(name); 110 pckgname = pckgname + "."; 111 112 File directory; 113 try { 114 directory = new File(URLDecoder.decode(url.getFile(), "UTF-8")); 115 } catch (UnsupportedEncodingException e) { 116 throw new RuntimeException(e); // should never happen 117 } 118 119 if (directory.exists()) { 120 logger.info("Searching for Demo classes in \"" 121 + directory.getName() + "\"."); 122 addAllFilesInDirectory(directory, classes, pckgname, recursive); 123 } else { 124 try { 125 // It does not work with the filesystem: we must 126 // be in the case of a package contained in a jar file. 127 logger.info("Searching for Demo classes in \"" + url + "\"."); 128 URLConnection urlConnection = url.openConnection(); 129 if (urlConnection instanceof JarURLConnection) { 130 JarURLConnection conn = (JarURLConnection) urlConnection; 131 132 JarFile jfile = conn.getJarFile(); 133 Enumeration e = jfile.entries(); 134 while (e.hasMoreElements()) { 135 ZipEntry entry = (ZipEntry) e.nextElement(); 136 Class result = load(entry.getName()); 137 if (result != null && !classes.contains(result)) { 138 classes.add(result); 139 } 140 } 141 } 142 } catch (IOException e) { 143 logger.logp(Level.SEVERE, this.getClass().toString(), 144 "find(pckgname, recursive, classes)", "Exception", e); 145 } catch (Exception e) { 146 logger.logp(Level.SEVERE, this.getClass().toString(), 147 "find(pckgname, recursive, classes)", "Exception", e); 148 } 149 } 150 return classes; 151 } 152 153 /** 154 * Load a class specified by a file- or entry-name 155 * 156 * @param name 157 * name of a file or entry 158 * @return class file that was denoted by the name, null if no class or does 159 * not contain a main method 160 */ load(String name)161 private Class load(String name) { 162 if (name.endsWith(".class") 163 && name.indexOf("Test") >= 0 164 && name.indexOf('$') < 0) { 165 String classname = name.substring(0, name.length() 166 - ".class".length()); 167 168 if (classname.startsWith("/")) { 169 classname = classname.substring(1); 170 } 171 classname = classname.replace('/', '.'); 172 173 try { 174 final Class<?> cls = Class.forName(classname); 175 cls.getMethod("main", new Class[] { String[].class }); 176 if (!getClass().equals(cls)) { 177 return cls; 178 } 179 } catch (NoClassDefFoundError e) { 180 // class has unresolved dependencies 181 return null; 182 } catch (ClassNotFoundException e) { 183 // class not in classpath 184 return null; 185 } catch (NoSuchMethodException e) { 186 // class does not have a main method 187 return null; 188 } catch (UnsupportedClassVersionError e){ 189 // unsupported version 190 return null; 191 } 192 } 193 return null; 194 } 195 196 /** 197 * Used to descent in directories, loads classes via {@link #load} 198 * 199 * @param directory 200 * where to search for class files 201 * @param allClasses 202 * add loaded classes to this collection 203 * @param packageName 204 * current package name for the diven directory 205 * @param recursive 206 * true to descent into subdirectories 207 */ addAllFilesInDirectory(File directory, Collection<Class> allClasses, String packageName, boolean recursive)208 private void addAllFilesInDirectory(File directory, 209 Collection<Class> allClasses, String packageName, boolean recursive) { 210 // Get the list of the files contained in the package 211 File[] files = directory.listFiles(getFileFilter()); 212 if (files != null) { 213 for (int i = 0; i < files.length; i++) { 214 // we are only interested in .class files 215 if (files[i].isDirectory()) { 216 if (recursive) { 217 addAllFilesInDirectory(files[i], allClasses, 218 packageName + files[i].getName() + ".", true); 219 } 220 } else { 221 Class result = load(packageName + files[i].getName()); 222 if (result != null && !allClasses.contains(result)) { 223 allClasses.add(result); 224 } 225 } 226 } 227 } 228 } 229 230 /** 231 * @return FileFilter for searching class files (no inner classes, only 232 * those with "Test" in the name) 233 */ getFileFilter()234 private FileFilter getFileFilter() { 235 return new FileFilter() { 236 public boolean accept(File pathname) { 237 return (pathname.isDirectory() && !pathname.getName().startsWith(".")) 238 || (pathname.getName().endsWith(".class") 239 && (pathname.getName().indexOf("Test") >= 0) 240 && pathname.getName().indexOf('$') < 0); 241 } 242 }; 243 } 244 245 private void startApp(final Object[] appClass){ 246 if (appClass == null){ 247 JOptionPane.showMessageDialog(rootPane, 248 "Please select a test from the list", 249 "Error", 250 JOptionPane.ERROR_MESSAGE); 251 return; 252 } 253 254 new Thread(new Runnable(){ 255 public void run(){ 256 for (int i = 0; i < appClass.length; i++) { 257 Class<?> clazz = (Class)appClass[i]; 258 try { 259 Object app = clazz.newInstance(); 260 if (app instanceof Application) { 261 if (app instanceof SimpleApplication) { 262 final Method settingMethod = clazz.getMethod("setShowSettings", boolean.class); 263 settingMethod.invoke(app, showSetting); 264 } 265 final Method mainMethod = clazz.getMethod("start"); 266 mainMethod.invoke(app); 267 Field contextField = Application.class.getDeclaredField("context"); 268 contextField.setAccessible(true); 269 JmeContext context = null; 270 while (context == null) { 271 context = (JmeContext) contextField.get(app); 272 Thread.sleep(100); 273 } 274 while (!context.isCreated()) { 275 Thread.sleep(100); 276 } 277 while (context.isCreated()) { 278 Thread.sleep(100); 279 } 280 } else { 281 final Method mainMethod = clazz.getMethod("main", (new String[0]).getClass()); 282 mainMethod.invoke(app, new Object[]{new String[0]}); 283 } 284 // wait for destroy 285 System.gc(); 286 } catch (IllegalAccessException ex) { 287 logger.log(Level.SEVERE, "Cannot access constructor: "+clazz.getName(), ex); 288 } catch (IllegalArgumentException ex) { 289 logger.log(Level.SEVERE, "main() had illegal argument: "+clazz.getName(), ex); 290 } catch (InvocationTargetException ex) { 291 logger.log(Level.SEVERE, "main() method had exception: "+clazz.getName(), ex); 292 } catch (InstantiationException ex) { 293 logger.log(Level.SEVERE, "Failed to create app: "+clazz.getName(), ex); 294 } catch (NoSuchMethodException ex){ 295 logger.log(Level.SEVERE, "Test class doesn't have main method: "+clazz.getName(), ex); 296 } catch (Exception ex) { 297 logger.log(Level.SEVERE, "Cannot start test: "+clazz.getName(), ex); 298 ex.printStackTrace(); 299 } 300 } 301 } 302 }).start(); 303 } 304 305 /** 306 * Code to create components and action listeners. 307 * 308 * @param classes 309 * what Classes to show in the list box 310 */ 311 private void setup(Vector<Class> classes) { 312 final JPanel mainPanel = new JPanel(); 313 mainPanel.setLayout(new BorderLayout()); 314 getContentPane().setLayout(new BorderLayout()); 315 getContentPane().add(mainPanel, BorderLayout.CENTER); 316 mainPanel.setBorder(new EmptyBorder(10, 10, 10, 10)); 317 318 final FilteredJList list = new FilteredJList(); 319 list.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); 320 DefaultListModel model = new DefaultListModel(); 321 for (Class c : classes) { 322 model.addElement(c); 323 } 324 list.setModel(model); 325 326 mainPanel.add(createSearchPanel(list), BorderLayout.NORTH); 327 mainPanel.add(new JScrollPane(list), BorderLayout.CENTER); 328 329 list.getSelectionModel().addListSelectionListener( 330 new ListSelectionListener() { 331 public void valueChanged(ListSelectionEvent e) { 332 selectedClass = list.getSelectedValues(); 333 } 334 }); 335 list.addMouseListener(new MouseAdapter() { 336 public void mouseClicked(MouseEvent e) { 337 if (e.getClickCount() == 2 && selectedClass != null) { 338 startApp(selectedClass); 339 } 340 } 341 }); 342 list.addKeyListener(new KeyAdapter() { 343 @Override 344 public void keyTyped(KeyEvent e) { 345 if (e.getKeyCode() == KeyEvent.VK_ENTER) { 346 startApp(selectedClass); 347 } else if (e.getKeyCode() == KeyEvent.VK_ESCAPE) { 348 dispose(); 349 } 350 } 351 }); 352 353 final JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.CENTER)); 354 mainPanel.add(buttonPanel, BorderLayout.PAGE_END); 355 356 final JButton okButton = new JButton("Ok"); 357 okButton.setMnemonic('O'); 358 buttonPanel.add(okButton); 359 getRootPane().setDefaultButton(okButton); 360 okButton.addActionListener(new ActionListener() { 361 public void actionPerformed(ActionEvent e) { 362 startApp(selectedClass); 363 } 364 }); 365 366 final JButton cancelButton = new JButton("Cancel"); 367 cancelButton.setMnemonic('C'); 368 buttonPanel.add(cancelButton); 369 cancelButton.addActionListener(new ActionListener() { 370 public void actionPerformed(ActionEvent e) { 371 dispose(); 372 } 373 }); 374 375 pack(); 376 center(); 377 } 378 379 private class FilteredJList extends JList { 380 private static final long serialVersionUID = 1L; 381 382 private String filter; 383 private ListModel originalModel; 384 385 public void setModel(ListModel m) { 386 originalModel = m; 387 super.setModel(m); 388 } 389 390 private void update() { 391 if (filter == null || filter.length() == 0) { 392 super.setModel(originalModel); 393 } 394 395 DefaultListModel v = new DefaultListModel(); 396 for (int i = 0; i < originalModel.getSize(); i++) { 397 Object o = originalModel.getElementAt(i); 398 String s = String.valueOf(o).toLowerCase(); 399 if (s.contains(filter)) { 400 v.addElement(o); 401 } 402 } 403 super.setModel(v); 404 if (v.getSize() == 1) { 405 setSelectedIndex(0); 406 } 407 revalidate(); 408 } 409 410 public String getFilter() { 411 return filter; 412 } 413 414 public void setFilter(String filter) { 415 this.filter = filter.toLowerCase(); 416 update(); 417 } 418 } 419 420 /** 421 * center the frame. 422 */ 423 private void center() { 424 Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize(); 425 Dimension frameSize = this.getSize(); 426 if (frameSize.height > screenSize.height) { 427 frameSize.height = screenSize.height; 428 } 429 if (frameSize.width > screenSize.width) { 430 frameSize.width = screenSize.width; 431 } 432 this.setLocation((screenSize.width - frameSize.width) / 2, 433 (screenSize.height - frameSize.height) / 2); 434 } 435 436 /** 437 * Start the chooser. 438 * 439 * @param args 440 * command line parameters 441 */ 442 public static void main(final String[] args) { 443 try { 444 UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); 445 } catch (Exception e) { 446 } 447 new TestChooser().start(args); 448 } 449 450 protected void start(String[] args) { 451 final Vector<Class> classes = new Vector<Class>(); 452 logger.info("Composing Test list..."); 453 addDisplayedClasses(classes); 454 setup(classes); 455 Class<?> cls; 456 setVisible(true); 457 } 458 459 protected void addDisplayedClasses(Vector<Class> classes) { 460 find("jme3test", true, classes); 461 } 462 463 private JPanel createSearchPanel(final FilteredJList classes) { 464 JPanel search = new JPanel(); 465 search.setLayout(new BorderLayout()); 466 search.add(new JLabel("Choose a Demo to start: Find: "), 467 BorderLayout.WEST); 468 final javax.swing.JTextField jtf = new javax.swing.JTextField(); 469 jtf.getDocument().addDocumentListener(new DocumentListener() { 470 public void removeUpdate(DocumentEvent e) { 471 classes.setFilter(jtf.getText()); 472 } 473 474 public void insertUpdate(DocumentEvent e) { 475 classes.setFilter(jtf.getText()); 476 } 477 478 public void changedUpdate(DocumentEvent e) { 479 classes.setFilter(jtf.getText()); 480 } 481 }); 482 jtf.addActionListener(new ActionListener() { 483 public void actionPerformed(ActionEvent e) { 484 selectedClass = classes.getSelectedValues(); 485 startApp(selectedClass); 486 } 487 }); 488 final JCheckBox showSettingCheck = new JCheckBox("Show Setting"); 489 showSettingCheck.setSelected(true); 490 showSettingCheck.addActionListener(new ActionListener() { 491 public void actionPerformed(ActionEvent e) { 492 showSetting = showSettingCheck.isSelected(); 493 } 494 }); 495 jtf.setPreferredSize(new Dimension(100, 25)); 496 search.add(jtf, BorderLayout.CENTER); 497 search.add(showSettingCheck, BorderLayout.EAST); 498 return search; 499 } 500 } 501