001    /* sillyview : a free model-view-controller system for Java
002     * Copyright (C) 2004 T.J. Willis
003     * 
004     * This program is free software; you can redistribute it and/or
005     * modify it under the terms of the GNU General Public License
006     * as published by the Free Software Foundation; either version 2
007     * of the License, or (at your option) any later version.
008     *      
009     * This program is distributed in the hope that it will be useful,
010     * but WITHOUT ANY WARRANTY; without even the implied warranty of
011     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
012     * GNU General Public License for more details.
013     *          
014     * You should have received a copy of the GNU General Public License
015     * along with this program; if not, write to the Free Software
016     * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111, USA.
017     *  
018     * $Header: /cvsroot/sillyview/sillyview/src/net/sourceforge/sillyview/HTMLPane.java,v 1.5 2004/05/22 07:58:27 tj_willis Exp $
019     */
020    package net.sourceforge.sillyview;
021    
022    import java.awt.event.*;
023    import java.io.*;
024    import java.net.URL;
025    import java.util.*;
026    import javax.swing.*;
027    import javax.swing.event.HyperlinkListener;
028    import javax.swing.plaf.*;
029    import javax.swing.plaf.metal.*;
030    import javax.swing.text.*;
031    import javax.swing.text.html.*;
032    import net.sourceforge.steelme.*;
033    import org.apache.log4j.*;
034    
035    /**
036     * This class is a convenience class for JEditorPanes used to show
037     * HTML content that are un-editable (so as to enable listening for
038     * hyperlink events) and handle UTF-8 characters gracefully.  It 
039     * wouldn't be strictly necessary were it not for some bugs in the 
040     * Java 1.4.2 StyledDocument api (getlength()=0).
041     *
042     * This class keeps its own copy of the EditorPane's text to work
043     * around the getLength, and subsequent getText problems.
044     *
045     * Important to note is that a HTMLPane is aware of and responsive to
046     * Swing Pluggable LookAndFeel colors.  JEditorPanes are ignorant of changes
047     * to LookAndFeel.  You can plug in your own LookAndFeel to StyleSheet
048     * converter by subclassing LookAndFeelToCSS and using HTMLPane's
049     * setConverter() method.  The default BasicLookAndFeelToCSS converter
050     * may well not handle your favorite LookAndFeel (Swing's default Metal
051     * LookAndFeel is supported) , and you may be forced to write a 
052     * LookAndFeelToCSS converter for it. 
053     *
054     * A "theme" stylesheet (the result of a conversion) does not override
055     * styles implemented directly in the HTML document or included in the
056     * HTML document.  It does override all other styles.
057     *
058     * @author <a href="mailto:tj_willis@users.sourceforge.net">T.J. Willis</a>
059     * @version 1.0
060     */
061    public class HTMLPane extends JEditorPane 
062    {
063        private String dString;
064        private LookAndFeelToCSS converter = new BasicLookAndFeelToCSS ();
065        private volatile boolean initialized = false;
066        private Category cat 
067            = Category.getInstance(HTMLPane.class.getName());
068        private boolean autodump = false;
069    
070       /**
071         * Creates a new <code>HTMLPane</code> instance with no text.
072         *
073         */
074        public HTMLPane () {
075            this("");
076            //initialized = true;
077        }
078    
079        /**
080         * Creates a new <code>HTMLPane</code> instance with the given text.
081         * Default text encoding is "text/html; UTF-8".
082         * @param text a <code>String</code> value
083         */
084        public HTMLPane (final String text) {
085            this(text != null ? text : "" ,null);
086            initialized = true;
087        }
088    
089        /**
090         * Creates a new <code>HTMLPane</code> instance with the given 
091         *  stylesheet.  Default text encoding is "text/html; UTF-8".
092         *
093         * @param theme a <code>StyleSheet</code> value
094         */
095        public HTMLPane (final StyleSheet theme) {
096            this("",theme);
097            //initialized = true;
098        }
099    
100        /**
101         * Creates a new <code>HTMLPane</code> instance with the given
102         * text and stylesheet.  Default text encoding is "text/html; UTF-8".
103         *
104         * @param text a <code>String</code> value
105         * @param theme a <code>StyleSheet</code> value
106         */
107        public HTMLPane (final String text, final StyleSheet theme) {
108            super ("text/html; UTF-8", text!= null ? text : "");
109            putClientProperty("charset","UTF-8");
110            initialized = true;
111            dString = text;
112            init ();
113            if(theme!=null){
114                // FIXME: overridable method
115                this.setTheme (theme);
116            }
117        }
118    
119        /**
120         * Creates a new <code>HTMLPane</code> instance showing the HTML
121         * page at the given URL and with no accessory stylesheet.
122         * Default text encoding is "text/html; UTF-8".
123         *
124         * @param initialPage an <code>URL</code> value
125         * @exception IOException if an error occurs
126         */
127        public HTMLPane (final URL initialPage) throws IOException {
128            this(initialPage,null);
129            initialized = true;
130        }
131    
132        /**
133         * Creates a new <code>HTMLPane</code> instance showing the HTML page
134         * at the given URL and with the given accesory stylesheet.
135         * Default text encoding is "text/html; UTF-8".
136         *
137         * @param initialPage an <code>URL</code> value
138         * @param theme a <code>StyleSheet</code> value
139         * @exception IOException if an error occurs
140         */
141        public HTMLPane (URL initialPage, StyleSheet theme) throws IOException {
142            super ("text/html; UTF-8", "");
143            initialized = true;
144            dString = getDocument (initialPage);
145            init ();
146            bugFixSetText (dString!=null ? dString : "");
147            if(theme!=null){
148                    // FIXME: overridable method
149                this.setTheme (theme);
150            }
151        }
152    
153        /**
154         * Sets the pane to non-editable, the content type to "text/html; UTF-8"
155         * and deactivates "autoformsubmission" so that HyperlinkListeners can
156         * handle form submission events.
157         *
158         */
159        private final void init () {
160            setEditable (false);
161            setContentType ("text/html; UTF-8");
162            HTMLEditorKit kitten = getHTMLEditorKit();
163            // jre1.5 --
164            // ensures that clicking a form submit btn creates a HyperlinkEvent
165            kitten.setAutoFormSubmission (false);
166        }
167    
168     
169    
170        /**
171         * Returns the stylesheet for the current document.
172         *
173         * @return a <code>StyleSheet</code> value
174         */
175        protected StyleSheet getDocumentStyleSheet () {
176            //if(cat!=null)cat.debug("Entering getDocumentStyleSheet");
177            Document doc = getDocument ();
178            if (doc==null || !(doc instanceof HTMLDocument)){
179                return null;
180            }
181            HTMLDocument htmld = (HTMLDocument) doc;
182            //if(cat!=null)cat.debug("Leaving getDocumentStyleSheet");
183            return htmld.getStyleSheet ();
184        }
185    
186        /**
187         * Returns the HTML document at the given URL as a String.
188         *
189         * @param documentURL a <code>String</code> value
190         * @return a <code>String</code> value
191         * @exception java.net.MalformedURLException if an error occurs
192         * @exception java.io.IOException if an error occurs
193         */
194        private static String getDocument (final String documentURL)
195            throws java.net.MalformedURLException, java.io.IOException 
196        {
197            URL url = new URL (documentURL);
198            if (url == null){
199                return null;
200            }
201            return getDocument (url);
202        }
203    
204        /**
205         * Returns the HTML document at the given URL as a String.
206         *
207         * @param u an <code>URL</code> value
208         * @return a <code>String</code> value
209         * @exception java.io.IOException if an error occurs
210         */
211        protected static String getDocument (final URL u) 
212            throws java.io.IOException 
213        {
214            StringBuffer cont = new StringBuffer ();
215    
216            InputStream is = u.openStream ();
217            InputStreamReader isr = new InputStreamReader (is);
218            BufferedReader in = new BufferedReader (isr);
219            String foo = in.readLine ();
220            while (foo != null) {
221                cont.append (foo).append ("\n");
222                //cont.append("\n"); //cont += foo +"\n";
223                foo = in.readLine ();
224            }
225            //is.flush();
226            is.close();
227            return cont.toString ();
228        }
229    
230        /**
231         * Adds the stylesheet at the given url as a "theme," i.e. it will
232         * not override rules in the document's own stylesheet.
233         *
234         * @param styleSheetUrl a <code>String</code> value
235         * @exception java.net.MalformedURLException if an error occurs
236         */
237        protected void setTheme (String styleSheetUrl)
238        throws java.net.MalformedURLException {
239            if (styleSheetUrl == null) {
240                this.setTheme ((URL) null);
241                return;
242            }
243    
244            URL ssurl = new URL (styleSheetUrl);
245            this.setTheme (ssurl);
246        }
247    
248        /**
249         * Adds the stylesheet at the given url as a "theme," i.e. it will
250         * not override rules in the document's own stylesheet.
251         *
252         * @param styleSheetUrl an <code>URL</code> value
253         */
254        private void setTheme (final URL styleSheetUrl) {
255            StyleSheet newSheet = new StyleSheet ();
256            newSheet.importStyleSheet (styleSheetUrl);
257            this.setTheme (newSheet);
258        }
259    
260        /**
261         * Adds the stylesheet at the given url as a "theme," i.e. it will
262         * not override rules in the document's own stylesheet.
263         *
264         * @param s2 a <code>StyleSheet</code> value
265         */
266        private void setTheme (final StyleSheet s2) {
267            try {
268                bugFixSetText (dString);
269    
270                if(s2!=null) {
271                    StyleSheet styles = getDocumentStyleSheet ();
272                    if(styles!=null) {
273                        styles.addStyleSheet(s2);
274                    } else {
275                        styles = s2;
276                    }
277                    final HTMLEditorKit kitten = getHTMLEditorKit();
278                    if(kitten!=null){
279                        kitten.setStyleSheet (styles);
280                        kitten.setAutoFormSubmission (false);
281                    }
282                }
283                if(autodump){
284                    dump();
285                }
286            } catch (Exception e) {
287                    if(cat!=null){
288                            cat.error("Odd HTMLPane exception #1: ",e);
289                    }
290            }
291        }
292    
293        
294        /**
295         * Returns the editorkit if it is a HTMLEditorKit, or null otherwise.
296         */
297        private HTMLEditorKit getHTMLEditorKit(){
298            EditorKit newKit = getEditorKit ();
299            if (!(newKit instanceof HTMLEditorKit)){
300                return null;
301            }
302            return (HTMLEditorKit) newKit;
303        }
304    
305    
306        /**
307         * Sets the text to the given value, tearing down the
308         * JEditorPane's backing model, which prevents peculiarities.
309         * See the documentation for JEditorPane.setText() for more
310         * information.
311         */
312        private final void bugFixSetText(final String text)
313        {
314            dString = (text != null? text : "");
315            StringReader rdr = null;
316            Document d0 = null;
317            rdr = new StringReader(dString);
318            d0 = getDocument ();
319    
320            // had some exceptions in super constructor
321            if(rdr == null || d0==null){
322                if(initialized){ 
323                    super.setText(dString);
324                }
325                return;
326            }
327            try {
328                HTMLDocument hDoc = (HTMLDocument) d0;
329                read(rdr,hDoc);
330            } catch (Exception e) {
331                cat.error("bugFixSetText exception: ",e);
332            }
333        }
334    
335        /**
336         * Sets the text and clears and clears any applied "theme"
337         * stylesheets.
338         *
339         * @param text a <code>String</code> value
340         */
341        public final void setText (final String text) {
342            //bugFixSetText(text);
343            dString = (text !=null ? text : "");
344            try {
345                this.setTheme ((StyleSheet) null);
346            } catch (Exception e) {
347                cat.error("Odd HTMLPane exception #2: ",e);
348                // "can't" happen
349            }
350        }
351    
352        // /**
353         // * Sets the text to the HTML document at the given URL.
354         // *
355         // * @param textUrl a <code>String</code> value
356         // * @exception java.net.MalformedURLException if an error occurs
357         // * @exception java.io.IOException if an error occurs
358         // */
359        // private void setTextUrl (final String textUrl)
360        // throws java.net.MalformedURLException, java.io.IOException {
361            // if (textUrl == null) {
362                // this.setText ("");
363                // return;
364            // }
365            // URL u = null;
366            // u = new URL (textUrl);
367            // this.setText (u);
368        // }
369    
370        /**
371         * Sets the text to the HTML document at the given URL.
372         *
373         * @param textURL an <code>URL</code> value
374         * @exception java.io.IOException if an error occurs
375         */
376        private void setText (final URL textURL) 
377            throws java.io.IOException 
378        {
379            StringBuffer cont = new StringBuffer ();
380    
381            InputStream is = textURL.openStream ();
382            InputStreamReader isr = new InputStreamReader (is);
383            BufferedReader in = new BufferedReader (isr);
384            String foo = in.readLine ();
385            while (foo != null) {
386                cont.append (foo).append ("\n");    // += foo +"\n";
387                foo = in.readLine ();
388            }
389            //is.flush();
390            is.close();
391            bugFixSetText (cont.toString ());
392    
393        }
394    
395        /**
396         * Dumps the current stylesheet for debugging.
397         *
398         */
399        public final void dump () {
400            HTMLEditorKit kitten = getHTMLEditorKit();
401            dump (kitten.getStyleSheet ());
402        }
403    
404        /**
405         * Setting autoDump to true causes the stylesheet to be printed
406         * every time it is changed.
407         *
408         * @param b a <code>boolean</code> value
409         */
410        public void setAutoDump(boolean newAutoDump){
411         autodump = newAutoDump;
412       }
413    
414        /**
415         * Sends all style rules to be printed out to System.out.
416         *
417         * @param styles a <code>StyleSheet</code> value
418         */
419        public final void dump (final StyleSheet styles) {
420            Enumeration rules = styles.getStyleNames ();
421            while (rules.hasMoreElements ()) {
422                String name = (String) rules.nextElement ();
423                Style rule = styles.getStyle (name);
424                System.out.println (rule.toString ());
425            }
426    
427        }
428    
429         private volatile LookAndFeel lastLookAndFeel = null;
430         private volatile javax.swing.plaf.TextUI lastUI = null;
431    
432        /**
433         * Describe <code>setUI</code> method here.
434         *
435         * @param newui a <code>javax.swing.plaf.TextUI</code> value
436         */
437        public void setUI(javax.swing.plaf.TextUI newui) {
438            //if(cat!=null)cat.debug("Entering setUI");
439            if(lastUI==newui) {
440              //if(cat!=null)cat.debug("Leaving setUI");
441              return;
442            }
443            LookAndFeel lf = UIManager.getLookAndFeel ();
444            if(lf!=null && lf != lastLookAndFeel ){
445                StyleSheet s = getStyleSheet (lf);
446                this.setTheme (s);
447            }
448            lastLookAndFeel = lf;
449            super.setUI(newui);
450            lastUI = newui;
451            //if(cat!=null)cat.debug("Leaving setUI");
452        }
453    
454    //      private volatile int updateLoop = 0;
455    
456    //     /**
457    //      * Describe <code>updateUI</code> method here.
458    //      *
459    //      */
460    //     public void updateUI () {
461    //         // seems to be an infinite loop after a few updateUI's
462    //         if(updateLoop>5) {
463    //             return;
464    //         }
465    //         updateLoop++;
466    //         System.out.println ("UpdateUI called: " + updateLoop);
467    
468    //         LookAndFeel lf = UIManager.getLookAndFeel ();
469    //         if(lf!=null && lf != lastLookAndFeel ){
470    //             StyleSheet s = getStyleSheet (lf);
471    //             setTheme (s);
472    //         }
473    //         lastLookAndFeel = lf;
474    //         super.updateUI ();
475    //         updateLoop--;
476    //     }
477    
478        /**
479         * Describe <code>getConverter</code> method here.
480         *
481         * @return a <code>LookAndFeelToCSS</code> value
482         */
483        public final LookAndFeelToCSS getConverter () {
484            return converter;
485        }
486    
487        /**
488         * Allows you to plug in a LookAndFeel to CSS converter of your
489         * choice.
490         *
491         * @param conv a <code>LookAndFeelToCSS</code> value
492         */
493        public final void setConverter (final LookAndFeelToCSS conv) {
494            converter = conv;
495        }
496    
497        private StyleSheet getStyleSheet (final LookAndFeel mlf) {
498                if (converter == null){
499                converter = new BasicLookAndFeelToCSS ();
500                }
501            assert converter != null:"Converter is null";
502            StyleSheet s = null;
503            try {
504                s = converter.convert (mlf);
505            } catch (UnsupportedLookAndFeelException e) {
506                cat.warn ("Exception : ", e);
507            } catch (NullPointerException ex) {
508                cat.warn ("Exception : ",ex);
509                ex.printStackTrace ();
510            }
511            return s;
512        }
513    
514        /**
515         * Add a HyperlinkListener to this pane.  This allows you to handle
516         * hyperlink events as well as form submission events.
517         *
518         * @param l a <code>HyperlinkListener</code> value
519         */
520        public void addHyperlinkListener (HyperlinkListener listener) {
521            super.addHyperlinkListener (listener);
522        }
523    
524        /**
525         * Tries to do super.doLayout() and warns if this causes an exception.
526         *
527         */
528        public void doLayout( ) {
529           //cat.debug("Entering doLayout");
530           try {
531              super.doLayout();
532           } catch (Exception e) {
533               cat.warn("Caught odd layout exception: ",e);
534           } 
535           //cat.debug("Leaving doLayout");
536        }
537    
538        public void forceValidate(){
539           validateTree();
540        }
541    }