001/* A viewer for HTML files.
002
003 Copyright (c) 2000-2014 The Regents of the University of California.
004 All rights reserved.
005 Permission is hereby granted, without written agreement and without
006 license or royalty fees, to use, copy, modify, and distribute this
007 software and its documentation for any purpose, provided that the above
008 copyright notice and the following two paragraphs appear in all copies
009 of this software.
010
011 IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY
012 FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
013 ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF
014 THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE POSSIBILITY OF
015 SUCH DAMAGE.
016
017 THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES,
018 INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
019 MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE
020 PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE UNIVERSITY OF
021 CALIFORNIA HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES,
022 ENHANCEMENTS, OR MODIFICATIONS.
023
024 PT_COPYRIGHT_VERSION_2
025 COPYRIGHTENDKEY
026
027 */
028package ptolemy.actor.gui;
029
030import java.awt.Dimension;
031import java.awt.Graphics;
032import java.awt.Graphics2D;
033import java.awt.geom.AffineTransform;
034import java.awt.print.PageFormat;
035import java.awt.print.Printable;
036import java.awt.print.PrinterException;
037import java.io.File;
038import java.io.IOException;
039import java.lang.reflect.Field;
040import java.lang.reflect.InvocationTargetException;
041import java.lang.reflect.Method;
042import java.net.MalformedURLException;
043import java.net.URL;
044
045import javax.swing.BoxLayout;
046import javax.swing.JEditorPane;
047import javax.swing.JScrollPane;
048import javax.swing.event.HyperlinkEvent;
049import javax.swing.event.HyperlinkListener;
050import javax.swing.text.html.HTMLDocument;
051import javax.swing.text.html.HTMLEditorKit;
052import javax.swing.text.html.HTMLFrameHyperlinkEvent;
053import javax.swing.text.html.StyleSheet;
054
055import ptolemy.gui.Top;
056import ptolemy.kernel.util.IllegalActionException;
057import ptolemy.kernel.util.StringAttribute;
058import ptolemy.util.ClassUtilities;
059import ptolemy.util.FileUtilities;
060import ptolemy.util.MessageHandler;
061
062///////////////////////////////////////////////////////////////////
063//// HTMLViewer
064
065/**
066 This class is a toplevel frame that can view HTML documents.
067 This class supports hyperlinks, and has a particular feature to
068 force hyperlinks to be opened in a browser.  To do that, specify
069 a hyperlink by giving a fragment (also called a reference) as "in_browser".
070 For example, the following URL will be opened in a browser:
071 <pre>
072 &lt;a href="http://ptolemy.eecs.berkeley.edu#in_browser"&gt;
073 </pre>
074
075 If the URL is <code>about:copyright</code>, then the copyrights will
076 be generated by {@link ptolemy.actor.gui.GenerateCopyrights#generateHTML(Configuration)}
077
078 <p>If the URL is <code>about:configuration</code>, then the
079 Ptolemy II configuration will be expanded by and the MoML of the
080 configuration will be returned.  This is a good way to test the
081 configuration.
082
083 <p>If the URL starts with <code>ptdoc:</code>, then the Ptolemy
084 documentation is opened.  For example
085 <pre>
086 &lt; a href="ptdoc:ptolemy.actor.gui.HTMLViewer"&gt;HTMLViewer&lt;/a&gt;
087 </pre>
088 will open the Ptolemy documentation for this class.  For details see
089 {@link ptolemy.vergil.basic.GetDocumentationAction}.
090
091 <p>If the URL starts with <code>$CLASSPATH</code> then the classpath
092 is searched.</p>
093
094 <p>This class supports printing and will save the text to a .html file.
095 The url that is viewed can be changed by calling the <i>setPage</i> method.
096
097 @author Steve Neuendorffer and Edward A. Lee
098 @version $Id$
099 @since Ptolemy II 1.0
100 @Pt.ProposedRating Yellow (eal)
101 @Pt.AcceptedRating Red (johnr)
102 */
103@SuppressWarnings("serial")
104public class HTMLViewer extends TableauFrame
105        implements Printable, HyperlinkListener {
106    /** Construct a blank viewer.
107     */
108    public HTMLViewer() {
109        _init();
110    }
111
112    /** Construct an empty top-level frame managed by the specified
113     *  tableau and the default status bar. After constructing this,
114     *  it is necessary to call setVisible(true) to make the frame appear.
115     *  It may also be desirable to call centerOnScreen().
116     *  @param tableau The managing tableau.
117     */
118    public HTMLViewer(Tableau tableau) {
119        super(tableau);
120        _init();
121    }
122
123    ///////////////////////////////////////////////////////////////////
124    ////                         public methods                    ////
125
126    /** Give a ptdoc: path, open the PtDoc viewer.
127     *  @param configuration The Configuration.
128     *  @param className The dot separated classname, such as
129     *  ptolemy.kernel.util.NamedObj.
130     *  @param context The controlling Effigy.
131     *  @exception IllegalActionException If thrown while searching
132     *  for the _getDocumentationActionClassName attribute in the
133     *  Configuration.
134     *  @exception ClassNotFoundException If the class named by the
135     *  _getDocumentationActionClassName attribute or
136     *  ptolemy.vergil.basic.GetDocumentationAction is not found.
137     *  @exception NoSuchMethodException If the class does not have
138     *  a getDocumentation(Configuration, String, Effigy) method.
139     *  @exception IllegalAccessException If thrown while calling
140     *  the getDocumentation() method.
141     *  @exception InvocationTargetException If thrown while calling
142     *  the getDocumentation() method.
143     */
144    public static void getDocumentation(Configuration configuration,
145            String className, Effigy context) throws IllegalActionException,
146            ClassNotFoundException, NoSuchMethodException,
147            IllegalAccessException, InvocationTargetException {
148        // Read the _getDocumentationActionClassName from
149        // the configuration and attempt to call it.
150        // If _getDocumentationActionClassName is not set,
151        // then default to vergil GetDocumentationAction.
152
153        // FIXME: Refactor this code, use DocApplicationSpecializer
154
155        StringAttribute getDocumentationActionClassNameStringAttribute = (StringAttribute) configuration
156                .getAttribute("_getDocumentationActionClassName",
157                        StringAttribute.class);
158        String getDocumentationActionClassName = null;
159        if (getDocumentationActionClassNameStringAttribute != null) {
160            getDocumentationActionClassName = getDocumentationActionClassNameStringAttribute
161                    .getExpression();
162        } else {
163            getDocumentationActionClassName = "ptolemy.vergil.basic.GetDocumentationAction";
164        }
165        Class getDocumentationActionClass = Class
166                .forName(getDocumentationActionClassName);
167        Method getDocumentationMethod = getDocumentationActionClass
168                .getMethod("getDocumentation", new Class[] {
169                        Configuration.class, String.class, Effigy.class });
170        //GetDocumentationAction.getDocumentation(configuration,
171        //        event.getDescription().substring(6), getEffigy());
172        getDocumentationMethod.invoke(null,
173                new Object[] { configuration, className, context });
174    }
175
176    /** Get the page displayed by this viewer.
177     *  @return The page displayed by this viewer.
178     *  @see #setPage(URL)
179     */
180    public URL getPage() {
181        return pane.getPage();
182    }
183
184    /** React to a hyperlink being clicked on in the rendered HTML.
185     *  This method opens the hyperlink URL in a new window, using
186     *  the configuration.  This means that hyperlinks can reference
187     *  any file that the configuration can open, including MoML files.
188     *  It is assumed this is called in the AWT event thread.
189     *  @param event The hyperlink event.
190     */
191    @Override
192    public void hyperlinkUpdate(HyperlinkEvent event) {
193        if (event.getEventType() == HyperlinkEvent.EventType.ENTERED) {
194            if (event.getURL() != null) {
195                // If the link was 'about:copyright',
196                // then getURL() returns null, but getDescription() works.
197                report(event.getURL().toString());
198            } else if (event.getDescription() != null) {
199                report(event.getDescription());
200            }
201        } else if (event.getEventType() == HyperlinkEvent.EventType.EXITED) {
202            report("");
203        } else if (event.getEventType() == HyperlinkEvent.EventType.ACTIVATED) {
204            URL newURL = event.getURL();
205
206            if (event.getDescription().startsWith("about:")) {
207                // Process "about:" hyperlinks
208                try {
209                    newURL = HTMLAbout.hyperlinkUpdate(event,
210                            getConfiguration());
211
212                } catch (Throwable throwable) {
213                    MessageHandler.error("Problem processing '"
214                            + event.getDescription() + "'.", throwable);
215                }
216            }
217
218            if (event.getDescription().startsWith("ptdoc:")) {
219                // Process "ptdoc:" hyperlinks
220                try {
221                    getDocumentation(getConfiguration(),
222                            event.getDescription().substring(6), getEffigy());
223                } catch (Throwable throwable) {
224                    MessageHandler.error("Problem processing '"
225                            + event.getDescription() + "'.", throwable);
226                }
227            }
228            // NOTE: It would be nice to use target="_browser" or some
229            // such, but this doesn't work. Targets aren't
230            // seen unless the link is inside a frame,
231            // regrettably.  An alternative might be to
232            // use the "userInfo" part of the URL,
233            // defined at http://www.ncsa.uiuc.edu/demoweb/url-primer.html
234            boolean useBrowser = false;
235
236            if (newURL != null) {
237                String ref = newURL.getRef();
238
239                if (ref != null) {
240                    useBrowser = ref.equals("in_browser");
241                }
242
243                String protocol = newURL.getProtocol();
244
245                if (protocol != null) {
246                    // Suggested mailto: extension from Paul Lieverse
247                    useBrowser |= protocol.equals("mailto");
248                }
249            } else {
250                // The URL is null, for some reason. This could happen,
251                // for example, if the HTML to be displayed is specified
252                // using setText() instead of setPage().  In this case,
253                // if relative URLs are to be supported, it is up to the
254                // using class to call setBase() to specify the relative
255                // URL.
256                try {
257                    newURL = new URL(_base, event.getDescription());
258                } catch (MalformedURLException e) {
259                    report("Link error: " + event.getDescription());
260                    return;
261                }
262            }
263
264            if (!useBrowser && event instanceof HTMLFrameHyperlinkEvent) {
265                // For some bizarre reason, when a link is within a frame,
266                // it needs to be handled differently than if its not in
267                // a frame.
268                HTMLFrameHyperlinkEvent frameHyperlinkEvent = (HTMLFrameHyperlinkEvent) event;
269                String target = frameHyperlinkEvent.getTarget();
270
271                if (target.equals("_browser")) {
272                    useBrowser = true;
273                } else if (!target.equals("_blank") && !target.equals("_top")) {
274                    // If the target is "_blank" or "_top", then we want to open
275                    // in a new window, so we defer to the below.
276                    HTMLDocument doc = (HTMLDocument) pane.getDocument();
277                    try {
278                        doc.processHTMLFrameHyperlinkEvent(frameHyperlinkEvent);
279                    } catch (Exception ex) {
280                        MessageHandler.error("Hyperlink reference failed", ex);
281                    }
282
283                    return;
284                }
285            }
286
287            try {
288                // If the URL is the same as the one we are currently in,
289                // then we are dealing with a link within the same file,
290                // so we want to stay in the same window.
291                if (getPage() != null
292                        && newURL.getFile().equals(getPage().getFile())) {
293                    pane.setPage(newURL);
294                } else {
295                    // Attempt to open in a new window.
296                    Configuration configuration = getConfiguration();
297
298                    // FIXME: Should detect target == "_blank" and open
299                    // in a new window, rather than always opening in a new
300                    // window.  However, regrettably, there appears to be
301                    // no way to access the target unless the event is an
302                    // instanceof HTMLFrameHyperlinkEvent, which it is only
303                    // if the HTML happens to be in a frame.  Moreover, it would
304                    // be tricky to do this because we would have to check that
305                    // the content type is "text/html" or "text/rtf", and we
306                    // would have to associate our tableau with a new effigy.
307                    // Nonetheless, it's perfectly doable if we can get the
308                    // target...
309                    if (configuration != null) {
310                        if (useBrowser && BrowserEffigy.staticFactory != null) {
311                            // Note that openModel will call MessageHandler
312                            // if there are problems, so there is no point
313                            // putting a try/catch block here.
314                            configuration.openModel(newURL, newURL,
315                                    newURL.toExternalForm(),
316                                    BrowserEffigy.staticFactory);
317                        } else {
318                            try {
319                                configuration.openModel(newURL, newURL,
320                                        newURL.toExternalForm());
321                            } catch (IOException ex) {
322                                // Try searching in the classpath in case the event description
323                                // starts with $CLASSPATH.
324                                // See http://bugzilla.ecoinformatics.org/show_bug.cgi?id=5194
325                                URL eventURL = null;
326                                String eventDescription = event
327                                        .getDescription();
328                                try {
329                                    eventURL = FileUtilities.nameToURL(
330                                            eventDescription, null, null);
331                                    if (eventURL == null) {
332                                        throw new NullPointerException(
333                                                "Could not find \""
334                                                        + eventDescription
335                                                        + "\"");
336                                    }
337                                    configuration.openModel(eventURL, eventURL,
338                                            eventURL.toExternalForm());
339                                } catch (Throwable throwable) {
340                                    if (eventDescription.indexOf(":/") == -1
341                                            || eventDescription
342                                                    .startsWith("/")) {
343                                        URL eventURL2 = null;
344                                        try {
345                                            // Try in the $CLASSPATH.
346                                            // One test is to view the docs in PNDirector from the website
347                                            // (no local docs) and then try to follow the links to other models.
348                                            if (eventDescription
349                                                    .startsWith("/")) {
350                                                eventDescription = eventDescription
351                                                        .substring(1);
352                                            }
353                                            String classpathEventDescription = "$CLASSPATH/"
354                                                    + eventDescription;
355                                            eventURL2 = FileUtilities.nameToURL(
356                                                    classpathEventDescription,
357                                                    null, null);
358                                            if (eventURL2 == null) {
359                                                throw new NullPointerException(
360                                                        "Could not find \""
361                                                                + classpathEventDescription
362                                                                + "\"");
363                                            }
364                                            configuration.openModel(eventURL2,
365                                                    eventURL2,
366                                                    eventURL2.toExternalForm());
367                                        } catch (Throwable throwable2) {
368                                            IOException exception = new IOException(
369                                                    "Failed to find " + newURL
370                                                            + ", also tried\n "
371                                                            + eventURL
372                                                            + " and\n"
373                                                            + eventURL2);
374                                            exception.initCause(ex);
375                                            throw exception;
376                                        }
377                                    }
378                                }
379                            }
380                        }
381                    } else {
382                        // If there is no configuration,
383                        // open in the same window.
384                        pane.setPage(newURL);
385                    }
386                }
387            } catch (Exception ex) {
388                MessageHandler.error("Hyperlink reference failed", ex);
389            }
390        }
391    }
392
393    // FIXME: This should be handled in Top...
394
395    /** Print the documentation to a printer.  The documentation will be
396     *  scaled to fit the width of the paper, growing to as many pages as
397     *  is necessary.
398     *  @param graphics The context into which the page is drawn.
399     *  @param format The size and orientation of the page being drawn.
400     *  @param index The zero based index of the page to be drawn.
401     *  @return PAGE_EXISTS if the page is rendered successfully, or
402     *   NO_SUCH_PAGE if pageIndex specifies a non-existent page.
403     *  @exception PrinterException If the print job is terminated.
404     */
405    @Override
406    public int print(Graphics graphics, PageFormat format, int index)
407            throws PrinterException {
408        Dimension dimension = pane.getSize();
409
410        // How much do we have to scale the width?
411        double scale = format.getImageableWidth() / dimension.getWidth();
412        double scaledHeight = dimension.getHeight() * scale;
413        int lastPage = (int) (scaledHeight / format.getImageableHeight());
414
415        // If we're off the end, then we're done.
416        if (index > lastPage) {
417            return Printable.NO_SUCH_PAGE;
418        }
419
420        AffineTransform at = new AffineTransform();
421        at.translate((int) format.getImageableX(),
422                (int) format.getImageableY());
423        at.translate(0, -(format.getImageableHeight() * index));
424        at.scale(scale, scale);
425
426        ((Graphics2D) graphics).transform(at);
427
428        pane.paint(graphics);
429        return Printable.PAGE_EXISTS;
430    }
431
432    /** Set the base URL for relative accesses.
433     *  @param base The base for relative hyperlink references.
434     */
435    public void setBase(URL base) {
436        _base = base;
437    }
438
439    /** Set the page displayed by this viewer to be that given by the
440     *  specified URL.
441     *  @param page The location of the documentation.
442     *  @exception IOException If the page cannot be read.
443     *  @see #getPage()
444     */
445    public void setPage(URL page) throws IOException {
446        URL jarURL = ClassUtilities.jarURLEntryResource(page.toString());
447        if (jarURL != null) {
448            // Under Java 1.7, JEditorPane.setPage() handles jar urls
449            // differently.  In Java 1.6, setPage() would correctly open
450            // jar:file:/Users/cxh/ptII/ptolemy/ptsupport.jar!/doc/mainVergilPtiny.htm
451            // even though doc/MainVergilPtiny.htm is in doc/docConfig.jar,
452            // not ptsupport.jar.  So, we look up the jar URL.
453            // See http://bugzilla.ecoinformatics.org/show_bug.cgi?id=5508
454            page = jarURL;
455        }
456        pane.setPage(page);
457    }
458
459    /** Override the base class to set the size of the scroll pane.
460     *  Regrettably, this is necessary because swing packers ignore
461     *  the specified size of a container. If this is not called in
462     *  the AWT event thread, then execution is deferred and executed
463     *  in that thread.
464     *  @param width The width of the scroll pane.
465     *  @param height The height of the scroll pane.
466     */
467    @Override
468    public void setSize(final int width, final int height) {
469        Runnable doSet = new Runnable() {
470            @Override
471            public void run() {
472                _setScrollerSize(width, height);
473                HTMLViewer.super.setSize(width, height);
474            }
475        };
476
477        Top.deferIfNecessary(doSet);
478    }
479
480    /** Set the text displayed by this viewer.
481     *  @param text The text to display.
482     */
483    public void setText(String text) {
484        pane.setText(text);
485    }
486
487    ///////////////////////////////////////////////////////////////////
488    ////                         public variables                  ////
489
490    /** The text pane. */
491    public JEditorPane pane = new JEditorPane();
492
493    ///////////////////////////////////////////////////////////////////
494    ////                         protected methods                 ////
495
496    /** Add the main content pane (for HTML).
497     */
498    protected void _addMainPane() {
499        // Default, which can be overridden by calling setSize().
500        _scroller.setPreferredSize(new Dimension(800, 600));
501        getContentPane().add(_scroller);
502    }
503
504    /** Set the scroller size.
505     *  @param width The width.
506     *  @param height The width.
507     */
508    protected void _setScrollerSize(final int width, final int height) {
509        _scroller.setPreferredSize(new Dimension(width, height));
510        _scroller.setSize(new Dimension(width, height));
511    }
512
513    /** Write the model to the specified file.  Note that this does not
514     *  defer to the effigy.
515     *  @param file The file to write to.
516     *  @exception IOException If the write fails.
517     */
518    @Override
519    protected void _writeFile(File file) throws IOException {
520        java.io.FileWriter fileWriter = null;
521
522        try {
523            fileWriter = new java.io.FileWriter(file);
524            fileWriter.write(pane.getText());
525        } finally {
526            if (fileWriter != null) {
527                fileWriter.close();
528            }
529        }
530    }
531
532    ///////////////////////////////////////////////////////////////////
533    ////                         protected variables               ////
534
535    /** The main scroll pane. */
536    protected JScrollPane _scroller;
537
538    ///////////////////////////////////////////////////////////////////
539    ////                         private methods                   ////
540
541    /** Initialize the HTMLViewer.
542     */
543    private void _init() {
544        getContentPane()
545                .setLayout(new BoxLayout(getContentPane(), BoxLayout.Y_AXIS));
546        pane.setContentType("text/html");
547        pane.setEditable(false);
548        pane.addHyperlinkListener(this);
549
550        if (_styleSheetURL != null) {
551            // If _styleSheetURL is non-null, we set the style sheet
552            // once and only once.  If try to do this in a static initializer,
553            // then the styles are wrong.
554            HTMLDocument doc = (HTMLDocument) pane.getDocument();
555            StyleSheet styleSheet = doc.getStyleSheet();
556            styleSheet.importStyleSheet(_styleSheetURL);
557            if (_HTMLEditorKit == null) {
558                _HTMLEditorKit = new HTMLEditorKit();
559            }
560            _HTMLEditorKit.setStyleSheet(styleSheet);
561            _styleSheetURL = null;
562        }
563
564        // http://mindprod.com/jgloss/antialiasing.html says that in
565        // java 1.5, this will turn on anti-aliased fonts
566        try {
567            // We use reflection so that this compiles everywhere.
568            Class swingUtilities = Class
569                    .forName("com.sun.java.swing.SwingUtilities2");
570            Field propertyField = swingUtilities
571                    .getDeclaredField("AA_TEXT_PROPERTY_KEY");
572            pane.putClientProperty(propertyField.get(null), Boolean.TRUE);
573        } catch (Throwable ex) {
574            // Ignore, we just wont have anti-aliased fonts then.
575        }
576
577        _scroller = new JScrollPane(pane);
578        _addMainPane();
579    }
580
581    ///////////////////////////////////////////////////////////////////
582    ////                         private variables                 ////
583
584    /** The base as specified by setBase(). */
585    private URL _base;
586
587    /** The HTMLEditorKit associated with this viewer. */
588    private static HTMLEditorKit _HTMLEditorKit;
589
590    /** The url that refers to $PTII/doc/default.css. */
591    private static URL _styleSheetURL;
592
593    static {
594        try {
595            Class refClass = Class.forName("ptolemy.kernel.util.NamedObj");
596            _styleSheetURL = refClass.getClassLoader()
597                    .getResource("doc/default.css");
598        } catch (Throwable ex) {
599            ex.printStackTrace();
600            // Ignore, we just use the wrong style sheets.
601        }
602
603    }
604
605    //     static {
606    //         try {
607    //             // We might be in the Swing Event thread, so
608    //             // Thread.currentThread().getContextClassLoader()
609    //             // .getResource(entry) probably will not work.
610    //             Class refClass = Class.forName("ptolemy.kernel.util.NamedObj");
611    //             URL styleSheetURL = refClass.getClassLoader()
612    //                 .getResource("doc/default.css");
613    //             if (styleSheetURL != null) {
614    //                 System.out.println("HTMLViewer: reading stylesheet "
615    //                         + styleSheetURL + "Instead of " + HTMLEditorKit.DEFAULT_CSS);
616
617    //                 StyleSheet styleSheet = htmlEditorKit.getStyleSheet();
618    //                 styleSheet.importStyleSheet(styleSheetURL);
619    //                 htmlEditorKit.setStyleSheet(styleSheet);
620    //             } else {
621    //                 System.out.println("Failed to read doc/default.css, so "
622    //                         + " the wrong style sheets will be used.");
623    //             }
624    //         } catch (Throwable ex) {
625    //             // Ignore, we just use the wrong style sheets.
626    //             ex.printStackTrace();
627    //         }
628    //    }
629}