001/**
002 * Copyright (c) 2008-2014 Ardor Labs, Inc.
003 *
004 * This file is part of Ardor3D.
005 *
006 * Ardor3D is free software: you can redistribute it and/or modify it
007 * under the terms of its license which may be found in the accompanying
008 * LICENSE file or at <http://www.ardor3d.com/LICENSE>.
009 */
010
011package com.ardor3d.example;
012
013import java.awt.BorderLayout;
014import java.awt.Color;
015import java.awt.Component;
016import java.awt.Dimension;
017import java.awt.FlowLayout;
018import java.awt.Font;
019import java.awt.event.ActionEvent;
020import java.io.BufferedReader;
021import java.io.File;
022import java.io.FileFilter;
023import java.io.IOException;
024import java.io.InputStream;
025import java.io.InputStreamReader;
026import java.io.UnsupportedEncodingException;
027import java.net.JarURLConnection;
028import java.net.URI;
029import java.net.URL;
030import java.net.URLConnection;
031import java.net.URLDecoder;
032import java.util.ArrayList;
033import java.util.Collections;
034import java.util.Comparator;
035import java.util.Enumeration;
036import java.util.HashMap;
037import java.util.LinkedHashMap;
038import java.util.List;
039import java.util.Locale;
040import java.util.Map.Entry;
041import java.util.MissingResourceException;
042import java.util.ResourceBundle;
043import java.util.Vector;
044import java.util.jar.JarEntry;
045import java.util.jar.JarFile;
046import java.util.logging.Level;
047import java.util.logging.Logger;
048import java.util.zip.ZipEntry;
049
050import javax.swing.AbstractAction;
051import javax.swing.Action;
052import javax.swing.BorderFactory;
053import javax.swing.ImageIcon;
054import javax.swing.JButton;
055import javax.swing.JComponent;
056import javax.swing.JFrame;
057import javax.swing.JLabel;
058import javax.swing.JOptionPane;
059import javax.swing.JPanel;
060import javax.swing.JScrollPane;
061import javax.swing.JSplitPane;
062import javax.swing.JTabbedPane;
063import javax.swing.JTextArea;
064import javax.swing.JTextField;
065import javax.swing.JToggleButton;
066import javax.swing.JToolBar;
067import javax.swing.JTree;
068import javax.swing.KeyStroke;
069import javax.swing.ScrollPaneConstants;
070import javax.swing.SwingConstants;
071import javax.swing.SwingUtilities;
072import javax.swing.UIManager;
073import javax.swing.WindowConstants;
074import javax.swing.event.DocumentEvent;
075import javax.swing.event.DocumentListener;
076import javax.swing.event.EventListenerList;
077import javax.swing.event.TreeModelEvent;
078import javax.swing.event.TreeModelListener;
079import javax.swing.event.TreeSelectionEvent;
080import javax.swing.event.TreeSelectionListener;
081import javax.swing.text.Document;
082import javax.swing.tree.DefaultTreeCellRenderer;
083import javax.swing.tree.TreeCellRenderer;
084import javax.swing.tree.TreeModel;
085import javax.swing.tree.TreePath;
086import javax.swing.tree.TreeSelectionModel;
087
088import com.ardor3d.util.resource.ResourceLocatorTool;
089
090/**
091 * starter for Ardor3D examples
092 */
093public class ExampleRunner extends JFrame {
094
095    private static final long serialVersionUID = 1L;
096    private final Logger logger = Logger.getLogger(ExampleRunner.class.getCanonicalName());
097    private final JTree tree;
098    private final ClassTreeModel model;
099    private final JSplitPane splitPane;
100    private final JLabel lDescription;
101    private final JLabel lStatus;
102    private final Action runSelectedAction;
103    private final Action browseSelectedAction;
104    private final Action firstMatchAction;
105    private final Action nextMatchAction;
106    private final Action previousMatchAction;
107    private final ErasableTextField tfPattern;
108    private final JTabbedPane tabbedPane;
109    private final DisplayConsole console;
110    private int maxHeapMemory = 64;
111    private final static String HEADER = "<html><body><h2 align=\"center\">Ardor3d Examples</h2>"
112            + "<p>Choose an example in the tree and select Run. Movement keys are: W, A, S, and D. Control view with left/right buttons.</p>"
113            + "<p align=\"center\"><img src=\"" + ResourceLocatorTool.getClassPathResource(ExampleRunner.class,
114                    "com/ardor3d/example/media/images/ardor3d_white_256.jpg")
115            + "\"></p></body></html>";
116    private static Comparator<Class<?>> classComparator = new Comparator<Class<?>>() {
117
118        @Override
119        public int compare(final Class<?> o1, final Class<?> o2) {
120            return o1.getCanonicalName().compareTo(o2.getCanonicalName());
121        }
122    };
123
124    public ExampleRunner() {
125        setTitle("Ardor3D SDK Examples");
126        setLayout(new BorderLayout());
127
128        model = new ClassTreeModel();
129        tree = new JTree(model);
130        tree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
131        tree.setCellRenderer(new ClassNameCellRenderer(model));
132        tfPattern = new ErasableTextField(10);
133        tfPattern.getDocument().addDocumentListener(new DocumentListener() {
134
135            @Override
136            public void removeUpdate(final DocumentEvent e) {
137                search();
138            }
139
140            @Override
141            public void insertUpdate(final DocumentEvent e) {
142                search();
143            }
144
145            @Override
146            public void changedUpdate(final DocumentEvent e) {
147                search();
148            }
149        });
150        final AbstractAction expandAction = new AbstractAction() {
151
152            private static final long serialVersionUID = 1L;
153
154            {
155                // putValue(Action.NAME, "Expand nodes");
156                putValue(Action.SMALL_ICON, getIcon("view-list-tree.png"));
157                putValue(Action.SHORT_DESCRIPTION, "Expand all branches");
158            }
159
160            @Override
161            public void actionPerformed(final ActionEvent e) {
162                if (((JToggleButton) e.getSource()).isSelected()) {
163                    for (int row = 0; row < tree.getRowCount(); row++) {
164                        tree.expandRow(row);
165                    }
166                } else {
167                    for (int row = 1; row < tree.getRowCount(); row++) {
168                        tree.collapseRow(row);
169                    }
170                }
171            }
172        };
173        final JToolBar toolbar = new JToolBar();
174        toolbar.setFloatable(false);
175        final JToggleButton btExpand = new JToggleButton(expandAction);
176        btExpand.setBorderPainted(false);
177        btExpand.setBorder(null);
178
179        lStatus = new JLabel(" ");
180        final JPanel pTree = new JPanel(new BorderLayout());
181        final JScrollPane scrTree = new JScrollPane(tree);
182        scrTree.setBorder(BorderFactory.createEmptyBorder(3, 3, 3, 3)); // border layout aligns with toolbarPanel
183        final JPanel pStatus = new JPanel(new FlowLayout(FlowLayout.LEADING));
184        pStatus.setBorder(BorderFactory.createEtchedBorder());
185        pStatus.add(lStatus);
186        pTree.add(scrTree);
187        pTree.add(toolbar, BorderLayout.NORTH);
188        pTree.add(pStatus, BorderLayout.SOUTH);
189
190        runSelectedAction = new AbstractAction() {
191
192            private static final long serialVersionUID = 1L;
193
194            {
195                putValue(Action.NAME, "Run");
196                putValue(Action.SMALL_ICON, getIcon("media-playback-start.png"));
197                putValue(Action.SHORT_DESCRIPTION, "Run the selected example Ctrl-R");
198                putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke("control R"));
199            }
200
201            @Override
202            public void actionPerformed(final ActionEvent e) {
203                runSelected();
204            }
205        };
206
207        browseSelectedAction = new AbstractAction() {
208
209            private static final long serialVersionUID = 1L;
210
211            {
212                putValue(Action.NAME, "Browse");
213                putValue(Action.SMALL_ICON, getIcon("world.png"));
214                putValue(Action.SHORT_DESCRIPTION,
215                        "View the source code. (Requires active Internet connection.) Ctrl-B");
216                putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke("control B"));
217            }
218
219            @Override
220            public void actionPerformed(final ActionEvent e) {
221                browseSelected();
222            }
223        };
224
225        previousMatchAction = new AbstractAction() {
226
227            private static final long serialVersionUID = 1L;
228            {
229                putValue(Action.NAME, "Previous match");
230                putValue(Action.SMALL_ICON, getIcon("go-up-search.png"));
231                putValue(Action.SHORT_DESCRIPTION, "Go to previous match Ctrl-P");
232                putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke("control P"));
233            }
234
235            @Override
236            public void actionPerformed(final ActionEvent e) {
237                navigateInMatches(-1);
238            }
239        };
240
241        nextMatchAction = new AbstractAction() {
242
243            private static final long serialVersionUID = 1L;
244            {
245                putValue(Action.NAME, "Next match");
246                putValue(Action.SMALL_ICON, getIcon("go-down-search.png"));
247                putValue(Action.SHORT_DESCRIPTION, "Go to next match Ctrl-N");
248                putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke("control N"));
249            }
250
251            @Override
252            public void actionPerformed(final ActionEvent e) {
253                navigateInMatches(1);
254            }
255        };
256
257        firstMatchAction = new AbstractAction() {
258
259            private static final long serialVersionUID = 1L;
260            {
261                putValue(Action.NAME, "First match");
262                putValue(Action.SMALL_ICON, getIcon("go-top-search.png"));
263                putValue(Action.SHORT_DESCRIPTION, "Go to first match Ctrl-F");
264                putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke("control F"));
265            }
266
267            @Override
268            public void actionPerformed(final ActionEvent e) {
269                tree.setSelectionRow(0);
270                navigateInMatches(1);
271            }
272        };
273
274        toolbar.add(btExpand);
275        toolbar.addSeparator();
276        toolbar.add(tfPattern);
277        toolbar.add(firstMatchAction);
278        toolbar.add(nextMatchAction);
279        toolbar.add(previousMatchAction);
280        toolbar.addSeparator();
281        toolbar.add(runSelectedAction);
282        toolbar.add(browseSelectedAction);
283
284        for (final Action action : new Action[] { firstMatchAction, nextMatchAction, previousMatchAction,
285                runSelectedAction, browseSelectedAction }) {
286            toolbar.getActionMap().put(action.getValue(Action.NAME), action);
287            toolbar.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)
288                    .put((KeyStroke) action.getValue(Action.ACCELERATOR_KEY), action.getValue(Action.NAME));
289        }
290
291        // add a handler for return (enter key) on the tree
292        tree.getActionMap().put(runSelectedAction.getValue(Action.NAME), runSelectedAction);
293        tree.getInputMap().put(KeyStroke.getKeyStroke("ENTER"), runSelectedAction.getValue(Action.NAME));
294
295        lDescription = new JLabel();
296        lDescription.setVerticalTextPosition(SwingConstants.TOP);
297        lDescription.setVerticalAlignment(SwingConstants.TOP);
298        lDescription.setHorizontalAlignment(SwingConstants.CENTER);
299        final JPanel pExample = new JPanel(new BorderLayout());
300        pExample.setBorder(BorderFactory.createEmptyBorder(2, 5, 2, 5));
301        pExample.add(lDescription);
302        console = new DisplayConsole();
303        tabbedPane = new JTabbedPane();
304        tabbedPane.setTabPlacement(SwingConstants.BOTTOM);
305        tabbedPane.addTab("Description", getIcon("declaration.png"), pExample);
306        tabbedPane.addTab("Console", getIcon("console.png"), console);
307
308        splitPane = new JSplitPane();
309        splitPane.setDividerLocation(300);
310        splitPane.setLeftComponent(pTree);
311        splitPane.setRightComponent(tabbedPane);
312        add(splitPane);
313
314        model.reload("com.ardor3d.example");
315        btExpand.doClick();
316        lStatus.setText("Examples: " + model.getSize());
317
318        tree.getSelectionModel().addTreeSelectionListener(new TreeSelectionListener() {
319
320            @Override
321            public void valueChanged(final TreeSelectionEvent e) {
322                updateDescription();
323                updateActionStatus();
324                tabbedPane.setSelectedIndex(0);
325            }
326        });
327
328        updateDescription();
329        updateActionStatus();
330    }
331
332    private void navigateInMatches(final int dir) {
333        int current = tree.getSelectionModel().getLeadSelectionRow();
334        for (;;) {
335            current += dir;
336            if (current >= tree.getRowCount()) {
337                current = 0;
338            } else if (current < 0) {
339                current = tree.getRowCount() - 1;
340            }
341            final TreePath tp = tree.getPathForRow(current);
342            final Object node = tp.getLastPathComponent();
343            if (node instanceof Class<?> && model.matches(node)) {
344                tree.setSelectionPath(tp);
345                tree.scrollPathToVisible(tp);
346                return;
347            }
348        }
349    }
350
351    private void search() {
352        final String pattern = tfPattern.getText();
353        final int matches = model.updateMatches(pattern);
354        tfPattern.setWarning(pattern.length() > 0 && matches == 0);
355        tree.repaint();
356        lStatus.setText("Examples: " + model.getSize() + (matches > 0 ? (" | matched: " + matches) : ""));
357        updateActionStatus();
358    }
359
360    private void updateActionStatus() {
361        final TreePath tp = tree.getSelectionPath();
362        final boolean canRun = tp != null && tp.getLastPathComponent() instanceof Class<?>;
363        runSelectedAction.setEnabled(canRun);
364        browseSelectedAction.setEnabled(canRun);
365        previousMatchAction.setEnabled(model.getMatchCount() > 0);
366        nextMatchAction.setEnabled(model.getMatchCount() > 0);
367        firstMatchAction.setEnabled(model.getMatchCount() > 0);
368    }
369
370    private void updateDescription() {
371        Class<?> selectedClass = null;
372        // just take the first selected node
373        final TreePath tp = tree.getSelectionPath();
374        if (tp != null) {
375            final Object selected = tp.getLastPathComponent();
376            if (selected instanceof Class<?>) {
377                selectedClass = (Class<?>) selected;
378            }
379        }
380        if (selectedClass == null) {
381            lDescription.setText(HEADER);
382        } else {
383            final Purpose purpose = selectedClass.getAnnotation(Purpose.class);
384            String imgURL = "";
385            String htmlDescription = "";
386            if (purpose != null) {
387                try {
388                    // Look for the example's thumbnail.
389                    final URL imageURL = ResourceLocatorTool.getClassPathResource(ExampleRunner.class,
390                            purpose.thumbnailPath());
391                    if (imageURL != null) {
392                        imgURL = "<br><img src=\"" + imageURL + "\">";
393                    }
394                } catch (final Exception ex) {
395                }
396
397                maxHeapMemory = purpose.maxHeapMemory();
398
399                htmlDescription = getHtmlDescription(purpose);
400            }
401            // default to Ardor3D logo if no image available.
402            if ("".equals(imgURL)) {
403                imgURL = "<img src=\"" + ResourceLocatorTool.getClassPathResource(ExampleRunner.class,
404                        "com/ardor3d/example/media/images/ardor3d_white_256.jpg") + "\"/>";
405            }
406
407            // Set the description HTML
408            lDescription.setText("<html><body><h2 align=\"center\">" + selectedClass.getSimpleName() + "</h2>" + "<p>"
409                    + htmlDescription + "</p>" + "<p align=\"center\">" + imgURL + "</p></body></html>");
410        }
411    }
412
413    private String getHtmlDescription(final Class<?> clazz) {
414        final Purpose purpose = clazz.getAnnotation(Purpose.class);
415        return getHtmlDescription(purpose);
416    }
417
418    private String getHtmlDescription(final Purpose purpose) {
419        if (purpose == null) {
420            return "";
421        }
422        try {
423            final ResourceBundle bundle = ResourceBundle.getBundle(purpose.localisationBaseFile(), Locale.getDefault(),
424                    ClassLoader.getSystemClassLoader());
425
426            return bundle.getString(purpose.htmlDescriptionKey());
427
428        } catch (final MissingResourceException e) {
429            logger.log(Level.WARNING, "no resource for " + purpose.localisationBaseFile(), e);
430        }
431        return "";
432    }
433
434    private void runSelected() {
435        // just take the first selected node
436        final TreePath tp = tree.getSelectionPath();
437        if (tp == null) {
438            return;
439        }
440        final Object selected = tp.getLastPathComponent();
441        if (!(selected instanceof Class<?>)) {
442            return;
443        }
444        new Thread() {
445
446            @Override
447            public void run() {
448                try {
449                    final Class<?> clazz = (Class<?>) selected;
450
451                    final boolean isWindows = System.getProperty("os.name").contains("Windows");
452                    final List<String> args = new ArrayList<>();
453                    args.add(isWindows ? "javaw" : "java");
454                    args.add("-Xmx" + maxHeapMemory + "M");
455                    args.add("-cp");
456                    args.add(System.getProperty("java.class.path"));
457                    args.add("-Djava.library.path=" + System.getProperty("java.library.path"));
458                    args.add(clazz.getCanonicalName());
459                    logger.info("start " + args.toString());
460                    final ProcessBuilder pb = new ProcessBuilder(args);
461                    pb.redirectErrorStream(true);
462                    final Process p = pb.start();
463                    final InputStream in = p.getInputStream();
464                    console.started(clazz.getCanonicalName(), p);
465                    new ConsoleStreamer(in, console).start();
466                } catch (final Exception ex) {
467                    JOptionPane.showMessageDialog(ExampleRunner.this, ex.toString());
468                }
469            }
470        }.start();
471    }
472
473    private void browseSelected() {
474        // just take the first selected node
475        final TreePath tp = tree.getSelectionPath();
476        if (tp == null) {
477            return;
478        }
479        final Object selected = tp.getLastPathComponent();
480        if (!(selected instanceof Class<?>)) {
481            return;
482        }
483
484        String className = ((Class<?>) selected).getCanonicalName();
485        className = className.replace('.', '/');
486        final String urlString = "http://ardorlabs.trac.cvsdude.com/Ardor3Dv1/browser/trunk/ardor3d-examples/src/main/java/"
487                + className + ".java";
488        try {
489            java.awt.Desktop.getDesktop().browse(new URI(urlString));
490        } catch (final Exception ex) {
491            ex.printStackTrace();
492            JOptionPane.showMessageDialog(ExampleRunner.this,
493                    "Unable to open URL: " + urlString + "\n" + ex.getMessage());
494        }
495    }
496
497    private static ImageIcon getIcon(final String name) {
498        return new ImageIcon(ResourceLocatorTool.getClassPathResource(ExampleRunner.class,
499                "com/ardor3d/example/media/icons/" + name));
500    }
501
502    interface SearchFilter {
503
504        public boolean matches(final Object value);
505
506        public int updateMatches(final String p);
507    }
508
509    class ClassTreeModel implements TreeModel, SearchFilter {
510
511        private final EventListenerList listeners = new EventListenerList();
512        private final LinkedHashMap<Package, Vector<Class<?>>> classes = new LinkedHashMap<>();
513        // the next two maps are for caching the status for the search filter
514        private final HashMap<Class<?>, Boolean> classMatches = new HashMap<>();
515        private final HashMap<Package, Boolean> packageMatches = new HashMap<>();
516        private String root = "all examples";
517        private FileFilter classFileFilter;
518        private int size;
519        private int matchCount = 0;
520
521        @Override
522        public void addTreeModelListener(final TreeModelListener l) {
523            listeners.add(TreeModelListener.class, l);
524        }
525
526        @Override
527        public Object getChild(final Object parent, final int index) {
528            if (root.equals(parent)) {
529                final Vector<Package> vec = new Vector<>(classes.keySet());
530                return vec.get(index);
531            }
532            final Vector<Class<?>> cl = classes.get(parent);
533            return cl == null ? null : cl.get(index);
534        }
535
536        @Override
537        public int getChildCount(final Object parent) {
538            if (root.equals(parent)) {
539                return classes.size();
540            }
541            final Vector<Class<?>> cl = classes.get(parent);
542            return cl == null ? 0 : cl.size();
543        }
544
545        @Override
546        public int getIndexOfChild(final Object parent, final Object child) {
547            if (root.equals(parent)) {
548                final Vector<Package> vec = new Vector<>(classes.keySet());
549                return vec.indexOf(child);
550            }
551            final Vector<Class<?>> cl = classes.get(parent);
552            return cl == null ? 0 : cl.indexOf(child);
553        }
554
555        @Override
556        public Object getRoot() {
557            return root;
558        }
559
560        public void addClassForPackage(final Class<?> clazz) {
561            logger.fine("found " + clazz);
562            if (clazz.equals(ExampleRunner.class)) {
563                return;
564            }
565            packageMatches.put(clazz.getPackage(), false);
566            classMatches.put(clazz, false);
567            Vector<Class<?>> cl = classes.get(clazz.getPackage());
568            if (cl == null) {
569                cl = new Vector<>();
570                classes.put(clazz.getPackage(), cl);
571            }
572            size++;
573            cl.add(clazz);
574            Collections.sort(cl, classComparator);
575        }
576
577        @Override
578        public int updateMatches(final String pattern) {
579            int numberMatches = 0;
580            final String lcPattern = pattern.toLowerCase();
581            packageMatches.clear();
582            TreePath firstPath = null;
583            int firstRow = Integer.MAX_VALUE;
584            for (final Entry<Class<?>, Boolean> entry : classMatches.entrySet()) {
585                final String className = entry.getKey().getSimpleName();
586                boolean matches = false;
587                boolean needsUpdate = false;
588                if ("".equals(pattern)) {
589                    matches = false;
590                } else {
591                    matches = !pattern.equals("") && className.toLowerCase().contains(lcPattern);
592                    if (!matches) {
593                        final String htmlDescription = getHtmlDescription(entry.getKey());
594                        matches = htmlDescription.toLowerCase().contains(lcPattern);
595                    }
596                }
597                if (entry.getValue() != matches) {
598                    needsUpdate = true;
599                }
600                entry.setValue(matches);
601                logger.fine(pattern + ": " + entry.getKey() + " set to " + matches);
602                if (matches) {
603                    final TreePath path = new TreePath(
604                            new Object[] { root, entry.getKey().getPackage(), entry.getKey() });
605                    final int row = tree.getRowForPath(path);
606                    if (row < firstRow) {
607                        firstPath = path;
608                        firstRow = row;
609                    }
610
611                    numberMatches++;
612                    final Package pkg = entry.getKey().getPackage();
613                    packageMatches.put(pkg, true);
614                    if (needsUpdate) {
615                        fireTreeNodeChanged(pkg);
616                        fireTreeNodeChanged(entry.getKey());
617                    }
618                }
619            }
620            if (firstPath == null) {
621                tree.clearSelection();
622            } else {
623                tree.setSelectionPath(firstPath);
624                tree.scrollPathToVisible(firstPath);
625            }
626            matchCount = numberMatches;
627            return numberMatches;
628        }
629
630        @Override
631        public boolean matches(final Object value) {
632            if (value instanceof Class<?>) {
633                return classMatches.get(value);
634            }
635            final Boolean res = packageMatches.get(value);
636            logger.fine("check " + value + " results in: " + res);
637            return res == null ? false : res;
638        }
639
640        @Override
641        public boolean isLeaf(final Object node) {
642            return node instanceof Class<?>;
643        }
644
645        @Override
646        public void removeTreeModelListener(final TreeModelListener l) {
647            listeners.remove(TreeModelListener.class, l);
648        }
649
650        @Override
651        public void valueForPathChanged(final TreePath path, final Object newValue) {
652            fireTreeNodeChanged(path);
653        }
654
655        /**
656         * @return FileFilter for searching class files (no inner classes)
657         */
658        private FileFilter getFileFilter() {
659            if (classFileFilter == null) {
660                classFileFilter = new FileFilter() {
661
662                    /**
663                     * @see FileFilter
664                     */
665                    @Override
666                    public boolean accept(final File pathname) {
667                        return (pathname.isDirectory()
668                                && (pathname.getName().length() == 0 || pathname.getName().charAt(0) != '.'))
669                                || (pathname.getName().endsWith(".class") && pathname.getName().indexOf('$') < 0);
670                    }
671                };
672            }
673            return classFileFilter;
674        }
675
676        /**
677         * Load a class specified by a file- or entry-name
678         *
679         * @param name
680         *            name of a file or entry
681         * @return class file that was denoted by the name, null if no class or does not contain a main method
682         */
683        private Class<?> load(final String name) {
684            if (name.endsWith(".class") && name.indexOf('$') < 0) {
685                String classname = name.substring(0, name.length() - ".class".length());
686
687                if (classname.length() > 0 && classname.charAt(0) == '/') {
688                    classname = classname.substring(1);
689                }
690                classname = classname.replace('/', '.');
691
692                try {
693                    final Class<?> cls = Class.forName(classname);
694                    cls.getMethod("main", new Class[] { String[].class });
695                    if (!getClass().equals(cls)) {
696                        return cls;
697                    }
698                } catch (final NoClassDefFoundError e) {
699                    // class has unresolved dependencies
700                    return null;
701                } catch (final ClassNotFoundException e) {
702                    // class not in classpath
703                    return null;
704                } catch (final NoSuchMethodException e) {
705                    // class does not have a main method
706                    return null;
707                }
708            }
709            return null;
710        }
711
712        /**
713         * Used to descent in directories, loads classes via {@link #load}
714         *
715         * @param directory
716         *            where to search for class files
717         * @param packageName
718         *            current package name for the diven directory
719         * @param recursive
720         *            true to descent into subdirectories
721         */
722        private void addAllFilesInDirectory(final File directory, final String packageName, final boolean recursive) {
723            // Get the list of the files contained in the package
724            logger.fine(directory + " -> " + packageName);
725            final File[] files = directory.listFiles(getFileFilter());
726            if (files != null) {
727                for (final File file : files) {
728                    // we are only interested in .class files
729                    if (file.isDirectory()) {
730                        if (recursive) {
731                            addAllFilesInDirectory(file, packageName + "." + file.getName(), true);
732                        }
733                    } else {
734                        final Class<?> result = load(packageName + "." + file.getName());
735                        if (result != null) {
736                            addClassForPackage(result);
737                        }
738                    }
739                }
740            }
741        }
742
743        protected void fireTreeChanged() {
744            for (final TreeModelListener l : listeners.getListeners(TreeModelListener.class)) {
745                l.treeStructureChanged(new TreeModelEvent(this, new String[] { root }));
746            }
747        }
748
749        protected void fireTreeNodeChanged(final TreePath tp) {
750            final Object obj = tp.getLastPathComponent();
751            final TreePath parentPath = tp.getParentPath();
752            final Object parentObj = parentPath.getLastPathComponent();
753            final TreeModelEvent ev = new TreeModelEvent(this, parentPath,
754                    new int[] { getIndexOfChild(parentObj, obj) }, new Object[] { obj });
755            for (final TreeModelListener l : listeners.getListeners(TreeModelListener.class)) {
756                l.treeNodesChanged(ev);
757            }
758        }
759
760        protected void fireTreeNodeChanged(final Package pkg) {
761            final TreeModelEvent ev = new TreeModelEvent(this, new Object[] { root },
762                    new int[] { getIndexOfChild(root, pkg) }, new Object[] { pkg });
763            for (final TreeModelListener l : listeners.getListeners(TreeModelListener.class)) {
764                l.treeNodesChanged(ev);
765            }
766        }
767
768        protected void fireTreeNodeChanged(final Class<?> clazz) {
769            final Package pkg = clazz.getPackage();
770            final TreeModelEvent ev = new TreeModelEvent(this, new Object[] { root, pkg },
771                    new int[] { getIndexOfChild(pkg, clazz) }, new Object[] { clazz });
772            for (final TreeModelListener l : listeners.getListeners(TreeModelListener.class)) {
773                l.treeNodesChanged(ev);
774            }
775        }
776
777        public void reload(final String pckgname) {
778            root = pckgname;
779            size = 0;
780            find(pckgname, true);
781            fireTreeChanged();
782        }
783
784        public int getSize() {
785            return size;
786        }
787
788        protected void find(final String pckgname, final boolean recursive) {
789            URL url;
790
791            // Translate the package name into an absolute path
792            String name = pckgname;
793            name = name.replace('.', '/');
794
795            // Get a File object for the package
796            // URL url = UPBClassLoader.get().getResource(name);
797            url = ResourceLocatorTool.getClassPathResource(ExampleRunner.class, name);
798            // URL url = ClassLoader.getSystemClassLoader().getResource(name);
799            // pckgname = pckgname + ".";
800
801            File directory;
802            try {
803                directory = new File(URLDecoder.decode(url.getFile(), "UTF-8"));
804            } catch (final UnsupportedEncodingException e) {
805                throw new RuntimeException(e); // should never happen
806            }
807
808            if (directory.exists()) {
809                logger.info("Searching for examples in \"" + directory.getPath() + "\".");
810                addAllFilesInDirectory(directory, pckgname, recursive);
811            } else {
812                try {
813                    // It does not work with the filesystem: we must
814                    // be in the case of a package contained in a jar file.
815                    logger.info("Searching for Demo classes in \"" + url + "\".");
816                    final URLConnection urlConnection = url.openConnection();
817                    if (urlConnection instanceof JarURLConnection) {
818                        final JarURLConnection conn = (JarURLConnection) urlConnection;
819
820                        final JarFile jfile = conn.getJarFile();
821                        final Enumeration<JarEntry> e = jfile.entries();
822                        while (e.hasMoreElements()) {
823                            final ZipEntry entry = e.nextElement();
824                            final Class<?> result = load(entry.getName());
825                            if (result != null) {
826                                addClassForPackage(result);
827                            }
828                        }
829                    }
830                } catch (final IOException e) {
831                    logger.logp(Level.SEVERE, this.getClass().toString(), "find(pckgname, recursive, classes)",
832                            "Exception", e);
833                } catch (final Exception e) {
834                    logger.logp(Level.SEVERE, this.getClass().toString(), "find(pckgname, recursive, classes)",
835                            "Exception", e);
836                }
837            }
838        }
839
840        public int getMatchCount() {
841            return matchCount;
842        }
843    }
844
845    private static class ClassNameCellRenderer implements TreeCellRenderer {
846
847        DefaultTreeCellRenderer defaultRenderer = new DefaultTreeCellRenderer();
848        JLabel classNameLabel = new JLabel(" ");
849
850        {
851            classNameLabel.setOpaque(true);
852        }
853        Font defaultFont = classNameLabel.getFont();
854        Font matchFont = defaultFont.deriveFont(Font.BOLD);
855        SearchFilter searchFilter;
856
857        public ClassNameCellRenderer(final SearchFilter searchFilter) {
858            this.searchFilter = searchFilter;
859        }
860
861        @Override
862        public Component getTreeCellRendererComponent(final JTree tree, final Object value, final boolean selected,
863                final boolean expanded, final boolean leaf, final int row, final boolean hasFocus) {
864            if (value == null) {
865                classNameLabel.setText("Null"); // Throw an exception?
866                classNameLabel.setFont(defaultFont);
867            } else {
868                if (value instanceof Class<?>) {
869                    final Class<?> clazz = (Class<?>) value;
870                    classNameLabel.setText(clazz.getSimpleName());
871                } else if (value instanceof Package) {
872                    String name = ((Package) value).getName();
873                    if (name.startsWith(tree.getModel().getRoot().toString())) {
874                        name = name.substring(tree.getModel().getRoot().toString().length() + 1);
875                    }
876                    classNameLabel.setText(name);
877                } else {
878                    classNameLabel.setText(value.toString());
879                }
880                classNameLabel.setFont(searchFilter.matches(value) ? matchFont : defaultFont);
881            }
882
883            if (selected) {
884                classNameLabel.setBackground(defaultRenderer.getBackgroundSelectionColor());
885                classNameLabel.setForeground(defaultRenderer.getTextSelectionColor());
886            } else {
887                classNameLabel.setBackground(defaultRenderer.getBackgroundNonSelectionColor());
888                classNameLabel.setForeground(defaultRenderer.getTextNonSelectionColor());
889            }
890            classNameLabel.setEnabled(tree.isEnabled());
891            return classNameLabel;
892        }
893    }
894
895    private static class ConsoleStreamer extends Thread {
896
897        InputStream is;
898        DisplayConsole console;
899
900        ConsoleStreamer(final InputStream is, final DisplayConsole console) {
901            this.is = is;
902            this.console = console;
903        }
904
905        @Override
906        public void run() {
907            try {
908                final InputStreamReader isr = new InputStreamReader(is);
909                final BufferedReader br = new BufferedReader(isr);
910                String line = null;
911                while ((line = br.readLine()) != null) {
912                    if (console != null) {
913                        console.appendLine(line);
914                    }
915                }
916            } catch (final IOException ioe) {
917                ioe.printStackTrace();
918            }
919        }
920    }
921
922    private static class DisplayConsole extends JPanel {
923
924        private static final long serialVersionUID = 1L;
925        private static final int MAX_CHARACTERS = 50000;
926        private final JTextArea textArea;
927        private final JScrollPane scrollPane;
928
929        public DisplayConsole() {
930            textArea = new JTextArea("Nothing started yet.");
931            textArea.setEditable(false);
932            textArea.setWrapStyleWord(true);
933            textArea.setLineWrap(true);
934
935            scrollPane = new JScrollPane(textArea);
936            scrollPane.setAutoscrolls(true);
937            scrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
938
939            setLayout(new BorderLayout());
940            add(scrollPane, BorderLayout.CENTER);
941        }
942
943        public void started(final String className, final Process process) {
944            textArea.setText(className + ": started...");
945            new Thread() {
946
947                @Override
948                public void run() {
949                    try {
950                        process.waitFor();
951                    } catch (final InterruptedException ex) {
952                    }
953                }
954            }.start();
955        }
956
957        public void appendLine(final String line) {
958            SwingUtilities.invokeLater(new Runnable() {
959                @Override
960                public void run() {
961                    String content = textArea.getText() + "\n" + line;
962                    if (content.length() > MAX_CHARACTERS) {
963                        content = content.substring(content.length() - MAX_CHARACTERS);
964                    }
965                    textArea.setText(content);
966                    textArea.setCaretPosition(textArea.getText().length());
967                }
968            });
969        }
970    }
971
972    private static class ErasableTextField extends JPanel {
973
974        private static final long serialVersionUID = 1L;
975        private final JTextField textField;
976        private final JButton btClear;
977        private final Color defaultTextBackground;
978
979        public ErasableTextField(final int len) {
980            super(new BorderLayout());
981            textField = new JTextField(len);
982            textField.setToolTipText("Type text here to find matching examples.");
983            defaultTextBackground = textField.getBackground();
984            btClear = new JButton(new AbstractAction() {
985
986                private static final long serialVersionUID = 1L;
987
988                {
989                    putValue(Action.SHORT_DESCRIPTION, "Clear search pattern");
990                    putValue(Action.SMALL_ICON, getIcon("edit-clear-locationbar-rtl.png"));
991                }
992
993                @Override
994                public void actionPerformed(final ActionEvent e) {
995                    textField.setText("");
996                }
997            });
998            btClear.setPreferredSize(new Dimension(20, 20));
999            btClear.setFocusable(false);
1000            btClear.setBorder(null);
1001            // keep it from taking all the space in a toolbar
1002            setMaximumSize(new Dimension(120, 20));
1003            add(textField);
1004            add(btClear, BorderLayout.EAST);
1005        }
1006
1007        public void setWarning(final boolean warn) {
1008            textField.setBackground(warn ? Color.yellow : defaultTextBackground);
1009        }
1010
1011        public Document getDocument() {
1012            return textField.getDocument();
1013        }
1014
1015        public String getText() {
1016            return textField.getText();
1017        }
1018    }
1019
1020    /**
1021     * @param args
1022     *            the arguments
1023     */
1024    public static void main(final String[] args) {
1025        try {
1026            UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
1027        } catch (final Exception e) {
1028        }
1029        final ExampleRunner app = new ExampleRunner();
1030        app.setIconImage(getIcon("ardor3d_white_24.png").getImage());
1031        app.setSize(800, 400);
1032        app.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
1033        app.setLocationRelativeTo(null);
1034        app.setVisible(true);
1035    }
1036}