001/**
002 * Copyright (c) 2011, The University of Southampton and the individual contributors.
003 * All rights reserved.
004 *
005 * Redistribution and use in source and binary forms, with or without modification,
006 * are permitted provided that the following conditions are met:
007 *
008 *   *  Redistributions of source code must retain the above copyright notice,
009 *      this list of conditions and the following disclaimer.
010 *
011 *   *  Redistributions in binary form must reproduce the above copyright notice,
012 *      this list of conditions and the following disclaimer in the documentation
013 *      and/or other materials provided with the distribution.
014 *
015 *   *  Neither the name of the University of Southampton nor the names of its
016 *      contributors may be used to endorse or promote products derived from this
017 *      software without specific prior written permission.
018 *
019 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
020 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
021 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
022 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
023 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
024 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
025 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
026 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
027 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
028 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
029 */
030/**
031 *
032 */
033package org.openimaj.demos;
034
035import java.awt.Color;
036import java.awt.Component;
037import java.awt.Dimension;
038import java.awt.Font;
039import java.awt.GridBagConstraints;
040import java.awt.GridBagLayout;
041import java.awt.Insets;
042import java.awt.event.ActionEvent;
043import java.awt.event.ActionListener;
044import java.io.IOException;
045import java.io.InputStream;
046import java.io.PrintWriter;
047import java.io.StringWriter;
048import java.io.Writer;
049import java.lang.reflect.Method;
050import java.net.URL;
051import java.util.HashMap;
052import java.util.Map;
053import java.util.Set;
054import java.util.Vector;
055
056import javax.swing.DefaultListCellRenderer;
057import javax.swing.ImageIcon;
058import javax.swing.JButton;
059import javax.swing.JFrame;
060import javax.swing.JLabel;
061import javax.swing.JList;
062import javax.swing.JOptionPane;
063import javax.swing.JPanel;
064import javax.swing.JScrollPane;
065import javax.swing.JTabbedPane;
066import javax.swing.event.ChangeEvent;
067import javax.swing.event.ChangeListener;
068import javax.swing.event.ListSelectionEvent;
069import javax.swing.event.ListSelectionListener;
070
071import org.apache.commons.io.IOUtils;
072import org.openimaj.util.processes.JavaProcess;
073import org.openimaj.util.processes.ProcessException;
074import org.reflections.Reflections;
075import org.xhtmlrenderer.simple.XHTMLPanel;
076import org.xhtmlrenderer.simple.extend.XhtmlNamespaceHandler;
077
078import com.uwyn.jhighlight.renderer.JavaXhtmlRenderer;
079
080/**
081 * This class provides a means for listing and running demos that have been
082 * automatically scanned from the classpath. The class looks for types that are
083 * annotated with the {@link Demo} annotation and displays these in a list. The
084 * source code should be available as a resource to this class, so that the
085 * source can be loaded and displayed.
086 * <p>
087 * Icons for demos should be resized so that they are 32x32 pixels and they
088 * should ideally be transparent PNGs.
089 * <p>
090 * Screenshots should be PNGs and should be resized to be 250 pixels wide.
091 *
092 * @author David Dupplaw (dpd@ecs.soton.ac.uk)
093 * @created 2nd November 2011
094 */
095public class Demos {
096        /**
097         * The location of the source code on the web. The class will look for the
098         * source code at this location if it cannot find it on disk.
099         */
100        public final static String OPENIMAJ_SRC_URL =
101                        "http://svn.code.sf.net/p/openimaj/code/trunk/demos/demos/src/main/java/";
102
103        /**
104         * This is the display for the demo runner.
105         *
106         * @author David Dupplaw (dpd@ecs.soton.ac.uk)
107         * @created 2nd November 2011
108         */
109        protected class DemoRunnerPanel extends JPanel {
110                /** */
111                private static final long serialVersionUID = 1L;
112
113                private JTabbedPane demoTabs = new JTabbedPane();
114                private Map<DemoPackage, JList<DemoObject>> demoTabMap =
115                                new HashMap<DemoPackage, JList<DemoObject>>();
116                private Map<DemoPackage, Vector<DemoObject>> demos =
117                                new HashMap<DemoPackage, Vector<DemoObject>>();
118                private JLabel demoTitle = new JLabel();
119                private JLabel demoAuthor = new JLabel();
120                private JLabel demoScreen = new JLabel();
121                private JLabel demoDescription = new JLabel();
122                private JButton demoRunButton = new JButton("Run Demo");
123                private JButton demoSourceButton = new JButton("See Source Code");
124
125                private DemoObject lastSelectedDemo = null;
126
127                /**
128                 * Default constructor that sets up the display
129                 */
130                public DemoRunnerPanel() {
131                        setLayout(new GridBagLayout());
132                        setBackground(Color.white);
133
134                        final GridBagConstraints gbc = new GridBagConstraints();
135                        gbc.gridx = gbc.gridy = 0;
136                        gbc.gridwidth = 1;
137                        gbc.fill = GridBagConstraints.BOTH;
138                        gbc.weightx = gbc.weighty = 1;
139
140                        // Set up a banner with the OpenIMAJ logo
141                        final JLabel openImajLogo = new JLabel(new ImageIcon(
142                                        Demos.class.getResource("/org/openimaj/demos/OpenIMAJ.png")));
143                        gbc.gridx = 1;
144                        gbc.weighty = 0;
145                        add(openImajLogo, gbc);
146
147                        // Set up the list of demos down the left-hand side of the display
148                        gbc.gridx = 0;
149                        gbc.gridy++;
150                        final int y = gbc.gridy;
151                        gbc.weighty = 1;
152                        gbc.insets = new Insets(8, 8, 8, 8);
153                        add(demoTabs, gbc);
154
155                        // Set up the panel down the right-hand side that displays
156                        // information about each demo.
157                        final JPanel p = new JPanel(new GridBagLayout());
158                        p.setOpaque(false);
159                        p.setPreferredSize(new Dimension(250, 600));
160                        p.setMaximumSize(new Dimension(250, 6000));
161                        p.setMinimumSize(new Dimension(250, 100));
162                        gbc.weighty = 0;
163                        gbc.fill = GridBagConstraints.BOTH;
164                        gbc.insets = new Insets(0, 0, 0, 0);
165                        p.add(demoTitle, gbc);
166                        gbc.gridy++;
167                        p.add(demoAuthor, gbc);
168                        gbc.gridy++;
169                        gbc.insets = new Insets(6, 0, 6, 0);
170                        p.add(demoDescription, gbc);
171                        gbc.gridy++;
172                        p.add(demoScreen, gbc);
173                        gbc.gridy++;
174                        gbc.insets = new Insets(1, 0, 1, 0);
175                        p.add(demoSourceButton, gbc);
176                        gbc.gridy++;
177                        p.add(demoRunButton, gbc);
178
179                        // Add a padding panel to the right-hand panel, so that the
180                        // text does spread out over the panel in an ugly way.
181                        gbc.weighty = 1;
182                        gbc.gridy++;
183                        final JPanel paddingPanel = new JPanel();
184                        paddingPanel.setOpaque(false);
185                        p.add(paddingPanel, gbc);
186
187                        gbc.gridx++;
188                        gbc.gridy = y;
189                        gbc.weighty = 1;
190                        gbc.weightx = 0;
191                        gbc.insets = new Insets(8, 8, 8, 8);
192                        add(p, gbc);
193
194                        demoTabs.addChangeListener(new ChangeListener()
195                        {
196                                @Override
197                                public void stateChanged(ChangeEvent e)
198                                {
199                                        @SuppressWarnings("unchecked")
200                                        final JList<DemoObject> d = (JList<DemoObject>) ((JScrollPane) demoTabs.getSelectedComponent())
201                                        .getViewport().getComponent(0);
202                                        if (d.getSelectedValue() != null)
203                                                updateDisplay(d.getSelectedValue());
204                                }
205                        });
206
207                        demoTitle.setFont(new Font("Arial", Font.BOLD, 14));
208                        demoDescription.setFont(new Font("Arial", Font.PLAIN, 10));
209
210                        // When run button is pressed, run the selected demo
211                        demoRunButton.addActionListener(new ActionListener()
212                        {
213                                @Override
214                                public void actionPerformed(ActionEvent e)
215                                {
216                                        try
217                                        {
218                                                final DemoObject obj = lastSelectedDemo;
219                                                if (obj != null)
220                                                {
221                                                        // runDemo( obj.demoClass, obj.annotation );
222                                                        runDemoNewJVM(obj.demoClass, obj.annotation);
223                                                }
224                                        }
225                                        catch (final Exception e1)
226                                        {
227                                                e1.printStackTrace();
228                                        }
229                                }
230                        });
231                        demoRunButton.setEnabled(false);
232
233                        // When the source button is pressed, display the source of the
234                        // selected demo.
235                        demoSourceButton.addActionListener(new ActionListener()
236                        {
237                                @Override
238                                public void actionPerformed(ActionEvent e)
239                                {
240                                        try
241                                        {
242                                                final DemoObject obj = lastSelectedDemo;
243
244                                                // Get the resource where the source code is stored
245                                                final String resource = "/" + obj.demoClass.getCanonicalName().
246                                                                replace(".", "/") + ".java";
247                                                System.out.println(resource);
248
249                                                // Read the source code from the resource
250                                                InputStream stream = Demos.class.getResourceAsStream(resource);
251
252                                                // If the stream is null it means the code isn't
253                                                // available.
254                                                // If that happens, we should try to read the source
255                                                // from
256                                                // the svn on openimaj.org.
257                                                if (stream == null)
258                                                {
259                                                        final URL u = new URL(OPENIMAJ_SRC_URL + resource);
260                                                        stream = u.openStream();
261                                                }
262
263                                                final String source = IOUtils.toString(stream, "ISO-8859-1");
264                                                // "<html><head></head><body><h1>Hello</h1></body></html>";
265
266                                                stream.close();
267
268                                                // Syntax highlight the source code in XHTML
269                                                final JavaXhtmlRenderer r = new JavaXhtmlRenderer();
270                                                final String h = r.highlight(obj.demoClass.getSimpleName(),
271                                                                source, "ISO-8859-1", false);
272
273                                                // Render the XHTML to an XHTML panel
274                                                final XHTMLPanel p = new XHTMLPanel();
275                                                p.setDocumentFromString(h, resource, new XhtmlNamespaceHandler());
276
277                                                // Stick the XHTMLPanel in a frame
278                                                final JFrame f = new JFrame();
279                                                f.setSize(800, 600);
280                                                f.setLocationRelativeTo(null);
281                                                f.getContentPane().add(new JScrollPane(p));
282                                                f.setVisible(true);
283                                        }
284                                        catch (final IOException e1)
285                                        {
286                                                e1.printStackTrace();
287                                        }
288                                }
289                        });
290                        demoSourceButton.setEnabled(false);
291                }
292
293                /**
294                 * Updates the information display of the demo.
295                 *
296                 * @param dObj
297                 *            The {@link DemoObject} for the selected demo
298                 */
299                private void updateDisplay(DemoObject dObj) {
300                        demoTitle.setText(dObj.annotation.title());
301                        demoDescription.setText("<html><p>" + dObj.annotation.description() + "</p></html>");
302                        demoAuthor.setText("By " + dObj.annotation.author());
303
304                        if (dObj.annotation.screenshot() != null)
305                                demoScreen.setIcon(new ImageIcon(
306                                                Demos.class.getResource(dObj.annotation.screenshot())));
307                        else
308                                demoScreen.setIcon(null);
309                }
310
311                /**
312                 * Add a demo to the list
313                 *
314                 * @param obj
315                 *            The object representing the demo
316                 */
317                public void addDemo(DemoObject obj) {
318                        JList<DemoObject> d = demoTabMap.get(obj.pkg);
319
320                        if (d == null) {
321                                final JList<DemoObject> r = new JList<DemoObject>();
322                                demoTabMap.put(obj.pkg, r);
323                                d = r;
324
325                                // When the list is clicked upon, update the demo information
326                                d.addListSelectionListener(new ListSelectionListener()
327                                {
328                                        @Override
329                                        public void valueChanged(ListSelectionEvent e)
330                                        {
331                                                demoRunButton.setEnabled(true);
332                                                demoSourceButton.setEnabled(true);
333                                                updateDisplay(lastSelectedDemo =
334                                                                r.getSelectedValue());
335                                        }
336                                });
337                                d.setCellRenderer(new IconListRenderer());
338
339                                demoTabs.addTab(
340                                                obj.pkg == null ? "Demos" : obj.pkg.title(),
341                                                                new JScrollPane(d));
342                        }
343
344                        Vector<DemoObject> dd = demos.get(obj.pkg);
345
346                        if (dd == null)
347                                demos.put(obj.pkg, dd = new Vector<Demos.DemoObject>());
348
349                        dd.add(obj);
350                        d.removeAll();
351                        d.setListData(dd);
352
353                        revalidate();
354                }
355        }
356
357        /**
358         * Used for each demo in the list.
359         *
360         * @author David Dupplaw (dpd@ecs.soton.ac.uk)
361         * @created 2nd November 2011
362         */
363        protected class DemoObject {
364                public Demo annotation;
365                public Class<?> demoClass;
366                public DemoPackage pkg;
367
368                public DemoObject(Class<?> c) {
369                        annotation = c.getAnnotation(Demo.class);
370                        demoClass = c;
371                        pkg = demoClass.getPackage().getAnnotation(DemoPackage.class);
372                }
373
374                @Override
375                public String toString() {
376                        return annotation.title();
377                }
378        }
379
380        /**
381         * A list renderer that adds an icon to the label.
382         *
383         * @author David Dupplaw (dpd@ecs.soton.ac.uk)
384         * @created 3rd November 2011
385         */
386        protected class IconListRenderer extends DefaultListCellRenderer {
387                private static final long serialVersionUID = 1L;
388
389                @Override
390                public Component getListCellRendererComponent(JList<?> list, Object value,
391                                int index, boolean isSelected, boolean cellHasFocus)
392                {
393                        final JLabel l = (JLabel) super.getListCellRendererComponent(
394                                        list, value, index, isSelected, cellHasFocus);
395
396                        final DemoObject o = (DemoObject) value;
397                        if (o.annotation.icon() != null) {
398                                URL u = getClass().getResource(o.annotation.icon());
399                                if (u == null)
400                                        u = getClass().getResource("/defaults/demo.png");
401                                l.setIcon(new ImageIcon(u));
402                        }
403
404                        return l;
405                }
406        }
407
408        /** The panel that will be used to display the list of dmeos */
409        private DemoRunnerPanel panel = new DemoRunnerPanel();
410
411        /**
412         * Default constructor
413         */
414        public Demos() {
415                try {
416                        // Get a list of the available demos
417                        final Set<Class<?>> c = findDemos();
418
419                        // Add the demos to the list
420                        for (final Class<?> cc : c)
421                                panel.addDemo(new DemoObject(cc));
422
423                        // Show the menu
424                        final JFrame f = new JFrame();
425                        f.getContentPane().add(panel);
426                        f.setSize(800, 600);
427                        f.setLocationRelativeTo(null);
428                        f.setVisible(true);
429                        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
430                } catch (final Exception e) {
431                        e.printStackTrace();
432                }
433        }
434
435        /**
436         * Finds the class files that have been annotated with the @Demo annotation.
437         */
438        private Set<Class<?>> findDemos() {
439                final Reflections reflections = new Reflections("org.openimaj.demos");
440                return reflections.getTypesAnnotatedWith(Demo.class);
441        }
442
443        /**
444         * Given a demo class file, will instantiate the demo and run its main
445         * method.
446         *
447         * @param clazz
448         *            The demo class file
449         */
450        @SuppressWarnings("unused")
451        private void runDemo(Class<?> clazz, Demo annotation) throws Exception {
452                try {
453                        final Method main = clazz.getDeclaredMethod("main", String[].class);
454                        System.out.println(main);
455                        main.invoke(null, (Object) annotation.arguments());
456                } catch (final Throwable t) {
457                        final String msg = String.format("Unexpected problem: %s",
458                                        getStackTrace(t.getCause()));
459                        JOptionPane.showMessageDialog(null, msg);
460                        throw new Exception(t);
461                }
462        }
463
464        /**
465         * Given a demo class file, instantiate the demo and run its main method in
466         * a new JVM
467         *
468         * @param clazz
469         *            The demo class file
470         */
471        private void runDemoNewJVM(final Class<?> clazz, Demo annotation) throws Exception {
472                final String[] jvmArgs = annotation.vmArguments();
473                final String[] appArgs = annotation.arguments();
474
475                new Thread() {
476                        @Override
477                        public void run() {
478                                try {
479                                        JavaProcess.runProcess(clazz, jvmArgs, appArgs);
480                                } catch (final ProcessException e) {
481                                        e.printStackTrace();
482                                }
483                        };
484                }.start();
485        }
486
487        /**
488         * Returns a string of a stack trace.
489         *
490         * @param aThrowable
491         *            The throwable to get a string for
492         * @return The throwable's stack as a string
493         */
494        private static String getStackTrace(Throwable aThrowable) {
495                final Writer result = new StringWriter();
496                final PrintWriter printWriter = new PrintWriter(result);
497                aThrowable.printStackTrace(printWriter);
498                return result.toString().substring(0, 1024) + "...";
499        }
500
501        /**
502         * Default main just starts the demo system.
503         *
504         * @param args
505         */
506        public static void main(String[] args) {
507                new Demos();
508        }
509}