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> demoTabMap =
115                                new HashMap<DemoPackage, JList>();
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                                        final JList d = (JList) ((JScrollPane) demoTabs.getSelectedComponent())
200                                                        .getViewport().getComponent(0);
201                                        if (d.getSelectedValue() != null)
202                                                updateDisplay((DemoObject) d.getSelectedValue());
203                                }
204                        });
205
206                        demoTitle.setFont(new Font("Arial", Font.BOLD, 14));
207                        demoDescription.setFont(new Font("Arial", Font.PLAIN, 10));
208
209                        // When run button is pressed, run the selected demo
210                        demoRunButton.addActionListener(new ActionListener()
211                        {
212                                @Override
213                                public void actionPerformed(ActionEvent e)
214                                {
215                                        try
216                                        {
217                                                final DemoObject obj = lastSelectedDemo;
218                                                if (obj != null)
219                                                {
220                                                        // runDemo( obj.demoClass, obj.annotation );
221                                                        runDemoNewJVM(obj.demoClass, obj.annotation);
222                                                }
223                                        }
224                                        catch (final Exception e1)
225                                        {
226                                                e1.printStackTrace();
227                                        }
228                                }
229                        });
230                        demoRunButton.setEnabled(false);
231
232                        // When the source button is pressed, display the source of the
233                        // selected demo.
234                        demoSourceButton.addActionListener(new ActionListener()
235                        {
236                                @Override
237                                public void actionPerformed(ActionEvent e)
238                                {
239                                        try
240                                        {
241                                                final DemoObject obj = lastSelectedDemo;
242
243                                                // Get the resource where the source code is stored
244                                                final String resource = "/" + obj.demoClass.getCanonicalName().
245                                                                replace(".", "/") + ".java";
246                                                System.out.println(resource);
247
248                                                // Read the source code from the resource
249                                                InputStream stream = Demos.class.getResourceAsStream(resource);
250
251                                                // If the stream is null it means the code isn't
252                                                // available.
253                                                // If that happens, we should try to read the source
254                                                // from
255                                                // the svn on openimaj.org.
256                                                if (stream == null)
257                                                {
258                                                        final URL u = new URL(OPENIMAJ_SRC_URL + resource);
259                                                        stream = u.openStream();
260                                                }
261
262                                                final String source = IOUtils.toString(stream, "ISO-8859-1");
263                                                // "<html><head></head><body><h1>Hello</h1></body></html>";
264
265                                                stream.close();
266
267                                                // Syntax highlight the source code in XHTML
268                                                final JavaXhtmlRenderer r = new JavaXhtmlRenderer();
269                                                final String h = r.highlight(obj.demoClass.getSimpleName(),
270                                                                source, "ISO-8859-1", false);
271
272                                                // Render the XHTML to an XHTML panel
273                                                final XHTMLPanel p = new XHTMLPanel();
274                                                p.setDocumentFromString(h, resource, new XhtmlNamespaceHandler());
275
276                                                // Stick the XHTMLPanel in a frame
277                                                final JFrame f = new JFrame();
278                                                f.setSize(800, 600);
279                                                f.setLocationRelativeTo(null);
280                                                f.getContentPane().add(new JScrollPane(p));
281                                                f.setVisible(true);
282                                        }
283                                        catch (final IOException e1)
284                                        {
285                                                e1.printStackTrace();
286                                        }
287                                }
288                        });
289                        demoSourceButton.setEnabled(false);
290                }
291
292                /**
293                 * Updates the information display of the demo.
294                 * 
295                 * @param dObj
296                 *            The {@link DemoObject} for the selected demo
297                 */
298                private void updateDisplay(DemoObject dObj) {
299                        demoTitle.setText(dObj.annotation.title());
300                        demoDescription.setText("<html><p>" + dObj.annotation.description() + "</p></html>");
301                        demoAuthor.setText("By " + dObj.annotation.author());
302
303                        if (dObj.annotation.screenshot() != null)
304                                demoScreen.setIcon(new ImageIcon(
305                                                Demos.class.getResource(dObj.annotation.screenshot())));
306                        else
307                                demoScreen.setIcon(null);
308                }
309
310                /**
311                 * Add a demo to the list
312                 * 
313                 * @param obj
314                 *            The object representing the demo
315                 */
316                public void addDemo(DemoObject obj) {
317                        JList d = demoTabMap.get(obj.pkg);
318
319                        if (d == null) {
320                                final JList r = new JList();
321                                demoTabMap.put(obj.pkg, r);
322                                d = r;
323
324                                // When the list is clicked upon, update the demo information
325                                d.addListSelectionListener(new ListSelectionListener()
326                                {
327                                        @Override
328                                        public void valueChanged(ListSelectionEvent e)
329                                        {
330                                                demoRunButton.setEnabled(true);
331                                                demoSourceButton.setEnabled(true);
332                                                updateDisplay(lastSelectedDemo =
333                                                                (DemoObject) r.getSelectedValue());
334                                        }
335                                });
336                                d.setCellRenderer(new IconListRenderer());
337
338                                demoTabs.addTab(
339                                                obj.pkg == null ? "Demos" : obj.pkg.title(),
340                                                new JScrollPane(d));
341                        }
342
343                        Vector<DemoObject> dd = demos.get(obj.pkg);
344
345                        if (dd == null)
346                                demos.put(obj.pkg, dd = new Vector<Demos.DemoObject>());
347
348                        dd.add(obj);
349                        d.removeAll();
350                        d.setListData(dd);
351
352                        revalidate();
353                }
354        }
355
356        /**
357         * Used for each demo in the list.
358         * 
359         * @author David Dupplaw (dpd@ecs.soton.ac.uk)
360         * @created 2nd November 2011
361         */
362        protected class DemoObject {
363                public Demo annotation;
364                public Class<?> demoClass;
365                public DemoPackage pkg;
366
367                public DemoObject(Class<?> c) {
368                        annotation = c.getAnnotation(Demo.class);
369                        demoClass = c;
370                        pkg = demoClass.getPackage().getAnnotation(DemoPackage.class);
371                }
372
373                @Override
374                public String toString() {
375                        return annotation.title();
376                }
377        }
378
379        /**
380         * A list renderer that adds an icon to the label.
381         * 
382         * @author David Dupplaw (dpd@ecs.soton.ac.uk)
383         * @created 3rd November 2011
384         */
385        protected class IconListRenderer extends DefaultListCellRenderer {
386                private static final long serialVersionUID = 1L;
387
388                @Override
389                public Component getListCellRendererComponent(JList list, Object value,
390                                int index, boolean isSelected, boolean cellHasFocus)
391                {
392                        final JLabel l = (JLabel) super.getListCellRendererComponent(
393                                        list, value, index, isSelected, cellHasFocus);
394
395                        final DemoObject o = (DemoObject) value;
396                        if (o.annotation.icon() != null) {
397                                URL u = getClass().getResource(o.annotation.icon());
398                                if (u == null)
399                                        u = getClass().getResource("/defaults/demo.png");
400                                l.setIcon(new ImageIcon(u));
401                        }
402
403                        return l;
404                }
405        }
406
407        /** The panel that will be used to display the list of dmeos */
408        private DemoRunnerPanel panel = new DemoRunnerPanel();
409
410        /**
411         * Default constructor
412         */
413        public Demos() {
414                try {
415                        // Get a list of the available demos
416                        final Set<Class<?>> c = findDemos();
417
418                        // Add the demos to the list
419                        for (final Class<?> cc : c)
420                                panel.addDemo(new DemoObject(cc));
421
422                        // Show the menu
423                        final JFrame f = new JFrame();
424                        f.getContentPane().add(panel);
425                        f.setSize(800, 600);
426                        f.setLocationRelativeTo(null);
427                        f.setVisible(true);
428                        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
429                } catch (final Exception e) {
430                        e.printStackTrace();
431                }
432        }
433
434        /**
435         * Finds the class files that have been annotated with the @Demo annotation.
436         */
437        private Set<Class<?>> findDemos() {
438                final Reflections reflections = new Reflections("org.openimaj.demos");
439                return reflections.getTypesAnnotatedWith(Demo.class);
440        }
441
442        /**
443         * Given a demo class file, will instantiate the demo and run its main
444         * method.
445         * 
446         * @param clazz
447         *            The demo class file
448         */
449        @SuppressWarnings("unused")
450        private void runDemo(Class<?> clazz, Demo annotation) throws Exception {
451                try {
452                        final Method main = clazz.getDeclaredMethod("main", String[].class);
453                        System.out.println(main);
454                        main.invoke(null, (Object) annotation.arguments());
455                } catch (final Throwable t) {
456                        final String msg = String.format("Unexpected problem: %s",
457                                        getStackTrace(t.getCause()));
458                        JOptionPane.showMessageDialog(null, msg);
459                        throw new Exception(t);
460                }
461        }
462
463        /**
464         * Given a demo class file, instantiate the demo and run its main method in
465         * a new JVM
466         * 
467         * @param clazz
468         *            The demo class file
469         */
470        private void runDemoNewJVM(final Class<?> clazz, Demo annotation) throws Exception {
471                final String[] jvmArgs = annotation.vmArguments();
472                final String[] appArgs = annotation.arguments();
473
474                new Thread() {
475                        @Override
476                        public void run() {
477                                try {
478                                        JavaProcess.runProcess(clazz, jvmArgs, appArgs);
479                                } catch (final ProcessException e) {
480                                        e.printStackTrace();
481                                }
482                        };
483                }.start();
484        }
485
486        /**
487         * Returns a string of a stack trace.
488         * 
489         * @param aThrowable
490         *            The throwable to get a string for
491         * @return The throwable's stack as a string
492         */
493        private static String getStackTrace(Throwable aThrowable) {
494                final Writer result = new StringWriter();
495                final PrintWriter printWriter = new PrintWriter(result);
496                aThrowable.printStackTrace(printWriter);
497                return result.toString().substring(0, 1024) + "...";
498        }
499
500        /**
501         * Default main just starts the demo system.
502         * 
503         * @param args
504         */
505        public static void main(String[] args) {
506                new Demos();
507        }
508}