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