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
33package jme3test;
34
35import com.jme3.app.Application;
36import com.jme3.app.SimpleApplication;
37import com.jme3.system.JmeContext;
38import java.awt.*;
39import java.awt.event.*;
40import java.io.File;
41import java.io.FileFilter;
42import java.io.IOException;
43import java.io.UnsupportedEncodingException;
44import java.lang.reflect.Field;
45import java.lang.reflect.InvocationTargetException;
46import java.lang.reflect.Method;
47import java.net.JarURLConnection;
48import java.net.URL;
49import java.net.URLConnection;
50import java.net.URLDecoder;
51import java.util.Collection;
52import java.util.Enumeration;
53import java.util.Vector;
54import java.util.jar.JarFile;
55import java.util.logging.Level;
56import java.util.logging.Logger;
57import java.util.zip.ZipEntry;
58import javax.swing.*;
59import javax.swing.border.EmptyBorder;
60import javax.swing.event.DocumentEvent;
61import javax.swing.event.DocumentListener;
62import javax.swing.event.ListSelectionEvent;
63import 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 */
70public 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     */
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     */
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     */
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     */
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     */
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