The HTMLPane sources In “Create Client-Side User Interfaces in HTML, Part 1,” I discussed in depth HTMLPane, a class that simplifies layout of client-side user interfaces by letting you specify them in HTML and providing a way to submit a form to your program rather than a Web server. Now I conclude this series by examining HTMLPane code. You should read last month’s article if you haven’t already.In this article, I also describe the Factory Method design pattern, which Java’s JEditorPane class uses heavily.Read the whole “Create Client-Side User Interfaces in HTML” series: Part 1: Make the JEditorPane useful (October 2003)Part 2: The HTMLPane sources (November 2003)Note: You can download this article’s associated source code from my Website.Extend JEditorPane with the Factory Method patternThe HTMLPane is an extension of javax.swing.JEditorPane that adapts it to do more than just display HTML text in a text control. Before I leap into the HTMLPane code, however, I need to explain how to customize a JEditorPane.First, a disclaimer: I’m not a fan of the javax.swing.* packages’ architecture. Swing is way too complex for what it does, and it’s a good example of going crazy with design patterns without considering whether the resulting system is usable or not. I would agree if you argued that the system I’m about to describe could have been better designed. Other design patterns (such as Strategy) would have been better choices than the patterns I used. JEditorPane makes heavy use of the Factory Method pattern. A factory method creates an object that implements a known interface, but you don’t know the actual class of the object being created. A derived-class override of the base-class factory method can supply a specialization of the default object.The problem with the factory-method approach to customization can be seen in the JEditorPane. It’s excruciating to change a JEditorPane‘s behavior in even trivial ways. Listing 1 shows what you must go through to add support for a custom tag to the JEditorPane class. I added support for a <today> tag, which displays today’s date on the output screen. It’s not a pretty picture.An EditorKit, used internally by the JEditorPane, parses HTML. To recognize a custom tag, you must provide your own EditorKit. You do this by passing the JEditorPane object a setEditorKit(myCustomKit) message; the most convenient way to do that is to extend JEditorKit and set things up in the constructor (Listing 1, line 17). By default the JEditorKit uses an EditorKit extension called HTMLEditorKit, which does almost all of the work we need to do. The main thing you must change is a ViewFactory, which the JEditorKit uses to build the visible representation of the HTML page. I created an HTMLEditorKit derivative called HTMLPane.SimplifiedHTMLPaneEditorKit that returns my custom view factory to the JEditorPane (Listing 1, line 23).The SimplifiedHTMLPane.CustomViewFactory (Listing 1, line 31), overrides a single method, create(). Every time the JEditorPane recognizes a new HTML element in the input, it calls create, passing it an Element object that represents the element actually found. The create() method extracts the tag name from the Element. If the tag is a <today> tag (recognized on line 41), create() returns an instance of yet another class, a View, whose createComponent() method returns the Component displayed on the screen in place of the <today> tag.Whew! As I said, Swing is not an example of simplicity and clarity in program design. This is a lot of complexity for an obvious modification. Be that as it may, this code does demonstrate the Factory Method design pattern in spades—the pattern is used three times in an overlapping fashion. Figure 1 shows the system’s structure. The design patterns are indicated with the “collaboration” symbol: a dashed oval labeled with the pattern name. The lines that connect to the oval indicate the classes that participate in the pattern.Let’s examine the first Factory Method reification: By default, an HTMLEditorKit creates an HTMLFactory by calling getViewFactory(). (getViewFactory() is the factory method.) SimplifiedHTMLPaneEditorKit extends HTMLEditorKit and overrides the factory method (getViewFactory()) to return an extension of HTMLFactory. In this reification, HTMLEditorKit has the role of Creator; HTMLFactory has the role of Product; and the two derived classes, SimplifiedHTMLPaneEditorKit and CustomViewFactory, have the roles of Concrete Creator and Concrete Product, respectively.Now we refocus. In the second reification, HTMLFactory and ComponentView have the Creator and Product roles. The factory method is create(). I extend HTMLFactory to create the Concrete Creator, CustomViewFactory, whose override of create() manufactures the Concrete Product, the anonymous inner class that extends ComponentView. Again, we refocus. In the third reification, ComponentView and the anonymous inner class have the roles of Creator and Product. The factory method is createComponent(). I extend ComponentView to create the Concrete Creator, the anonymous inner class, whose override of createComponent() manufactures the Concrete Product, a JLabel.So, depending on how you look at it, HTMLFactory is either a Product or a Creator, and CustomViewFactory is either a Concrete Product or a Concrete Creator. By the same token, ComponentView is itself either a Creator or Product, and so on. This example graphically demonstrates how design patterns appear in the real world, all jumbled up with each other in complex ways. It’s rare that a design pattern exists in the splendid isolation you would expect from reading the Gang of Four book.Listing 1. Using Factory Method 1 import java.awt.*; 2 import javax.swing.*; 3 import javax.swing.text.*; 4 import javax.swing.text.html.*; 5 import java.util.Date; 6 import java.text.DateFormat; 7 8 public class SimplifiedHTMLPane extends JEditorPane 9 { 10 public SimplifiedHTMLPane() 11 { registerEditorKitForContentType( "text/html", 12 "com.holub.ui.SimplifiedHTMLPane$SimplifiedHTMLPaneEditorKit" ); 13 14 setEditorKitForContentType( "text/html", 15 new SimplifiedHTMLPaneEditorKit() ); 16 17 setEditorKit( new SimplifiedHTMLPaneEditorKit() ); 18 19 setContentType ( "text/html" ); 20 setEditable ( false ); 21 } 22 23 public class SimplifiedHTMLPaneEditorKit extends HTMLEditorKit 24 { 25 public ViewFactory getViewFactory() 26 { return new CustomViewFactory(); 27 } 28 //... 29 } 30 31 private final class CustomViewFactory extends HTMLEditorKit.HTMLFactory 32 { 33 public View create(Element element) 34 { HTML.Tag kind = (HTML.Tag)( 35 element.getAttributes().getAttribute( 36 javax.swing.text.StyleConstants.NameAttribute) ); 37 38 if( kind instanceof HTML.UnknownTag 39 && element.getAttributes().getAttribute(HTML.Attribute.ENDTAG)== null) 40 { // <today> tag 41 if( element.getName().equals("today") ) 42 { return new ComponentView(element) 43 { protected Component createComponent() 44 { DateFormat formatter 45 = DateFormat.getDateInstance( DateFormat.MEDIUM ); 46 return new JLabel( formatter.format( new Date() ) ); 47 48 } 49 }; 50 } 51 } 52 return super.create(element); 53 } 54 } 55 } If you’re mystified by why everything is so complex, consider that the Swing text packages are extraordinarily flexible—in fact, way more flexible than necessary. Swing is a great example of how support for imaginary requirements (as opposed to requirements demanded by real users) dramatically impacts the system’s maintainability. The degree of flexibility built into Swing is an unrealistic requirement—a “feature” nobody asked for or needs. Proof of my claim that you don’t need to customize Swing is that no one I know does it. Though you might argue that nobody can figure out how to do it, you can also argue that nobody has been lobbying for making customization easier. This unnecessary complexity only increases development time, with no obvious payback.To make matters worse, Factory Method forces you to use implementation inheritance just to get control over object creation. This is really a bogus use of extends because the derived class doesn’t extend the base class; it adds no new functionality, for example. This inappropriate use of the extends relationship leads to the fragile-base-class problem I discussed in a past Java Toolbox column (see “Why extends Is Evil”).HTMLPane: The gory detailsLet’s move on to HTMLPane.java. The file is way bigger than I like—weighing in at 1,500 lines, unexpurgated. (I dropped a few of the larger Javadoc comments that discuss the material covered in last month’s column.) Generally, I like to keep classes much smaller than this one, but there’s not much scope for shrinking things short of moving all the private inner classes—that really should be private—out to package scope. I opted for a larger file to avoid polluting the package with classes that weren’t relevant outside of the HTMLPane implementation. Given the size, let’s look at it in pieces. The first chunk, shown below, contains usual field definitions; most of them are self-explanatory. (I’ll come back to the ones that aren’t self-explanatory in a moment.): 1 package com.holub.ui.HTML; 2 3 import com.holub.net.UrlUtil; 4 import com.holub.tools.Log; 5 import com.holub.ui.AncestorAdapter; 6 7 import com.holub.ui.HTML.TagBehavior; 8 import com.holub.ui.HTML.FilterFactory; 9 10 import java.io.*; 11 import java.util.*; 12 import java.net.*; 13 import java.awt.*; 14 import java.awt.event.*; 15 import java.util.logging.*; 16 import java.util.regex.*; 17 import javax.swing.*; 18 import javax.swing.text.*; // View Element ViewFactory 19 import javax.swing.text.html.*; 20 import javax.swing.event.*; 21 import javax.swing.border.*; 22 23 // See full documentation for this class in last month's article. 24 /**...*/ 25 26 public class HTMLPane extends JEditorPane 27 { 28 private FilterFactory filterProvider = FilterFactory.NULL_FACTORY; 29 30 private static final Logger log = Logger.getLogger("com.holub.ui"); 31 32 /** A map of actual host names to replacement names, set by 33 * {@link #addHostMapping} 34 */ 35 private static Map hostMap = null; 36 37 /** Maps tags to Publishers of {@linkplain TagHandler handlers} for 38 * that tag 39 */ 40 private Map tagHandlers = 41 Collections.synchronizedMap(new HashMap()); 42 43 /** A list of all components provided by a TagHandler that 44 * support the {@link TagBehavior} interface. 45 */ 46 47 private ActionListener actionListeners = null; 48 49 50 /** A list of all JComponents that act as stand in for custom 51 * tags that also implement TagBehavior. 52 */ 53 private Collection contributors = new LinkedList(); 54 55 /** 56 * The name used as a key to get the tag-name attribute out of 57 * the attributes passed to a TagHandler object. 58 */ 59 public static final String TAG_NAME = "<tagName>"; 60 61 /** 62 * All controls created from HTML tags (except for multiline text input) 63 * are positioned so that they're aligned 64 * properly with respect to the text baseline. This way a radio button, 65 * for example, will line up with the text next to it. If you create 66 * a control of your own to be displayed in place of to a custom tag, 67 * you may want to issue a: 68 * <PRE> 69 * widget.setAlignmentY( BASELINE_ALIGNMENT ); 70 * </PRE> 71 * request to get it aligned the same way as the standard controls 72 */ 73 74 public static final float BASELINE_ALIGNMENT = 0.70F; 75 76 HTMLPane constructorsNext come the constructors, shown in the code below.The critical call is the one to setEditorKit( new HTMLPaneEditorKit() ), which installs my custom editor kit instead of the default one. The main problem I solve in the rest of the constructor is graceful shutdown. I want to discover when the containing window shuts down, but the AncestorListener class, unfortunately, doesn’t give me this information. Consequently, I trap the ancestorAdded() method, which is called when the HTMLPane‘s container becomes visible, and traverse up the runtime containment hierarchy looking for a container that’s also a java.awt.Window. I then hook up a window-closing event listener to this ancestor to find out when it shuts down. At shutdown time, I call handlePageShutdown() to do the work. This method, declared just beneath the constructor on line 138, notifies all the widgets that represent custom tags (discussed last month) that a destroy() operation is in progress so they can clean up local resources if necessary.The second constructor in the following snippet installs a few custom tag handlers. I showed you one of these last month and have included the others in the source distribution. (I won’t discuss them any further here.): 77 78 /** 79 * Create an empty pane. Populate it by calling 80 * {@link JEditorPane#setPage(URL)} or {@link JEditorPane#setText(String)} 81 * <PRE> 82 * HTMLPane form = new HTMLPane(); 83 * form.setPage( new URL("file://test.html") ); 84 * </PRE> 85 * (For reasons that are not clear to me, the URL and String versions 86 * of the base-class constructors don't work when called from 87 * a derived-class constructor, so these base-class constructors 88 * are not exposed here.) 89 */ 90 91 public HTMLPane() 92 { 93 registerEditorKitForContentType( "text/html", 94 "com.holub.ui.HTMLPane$HTMLPaneEditorKit" ); 95 96 //setEditorKitForContentType( "text/html", new HTMLPaneEditorKit() ); 97 98 setEditorKit( new HTMLPaneEditorKit() ); 99 100 setContentType ( "text/html" ); 101 addHyperlinkListener ( new HyperlinkHandler() ); 102 setEditable ( false ); 103 104 // Set up to shut down pages gracefully when the parent window 105 // shuts down. Note that this doesn't work when somebody closes 106 // the outermost frame by clicking the "X" box, because the 107 // handler for that event (which typically calls System.exit()) is 108 // processed before the ancestor event is generated. This omission 109 // is not a big deal, since the program is shutting down anyway, 110 // but don't put a println in the following code and be surprised 111 // when it doesn't get executed. 112 113 addAncestorListener 114 ( new AncestorAdapter() 115 { public void ancestorAdded(AncestorEvent e) 116 { for( Component c=HTMLPane.this; c!=null; c=c.getParent()) 117 { if( c instanceof Window ) 118 { ((Window)c).addWindowListener 119 ( new WindowAdapter() 120 { public void windowClosing(WindowEvent e) 121 { handlePageShutdown(); 122 } 123 } 124 ); 125 break; 126 } 127 } 128 } 129 } 130 ); 131 } 132 133 /** 134 * Since destructors aren't possible, provide a method to handle page 135 * shutdown. Notify the JComponents associated with the custom tags that 136 * the page is shutting down and do some local housekeeping. 137 */ 138 protected void handlePageShutdown() 139 { 140 for( Iterator i = contributors.iterator(); i.hasNext(); ) 141 ((TagBehavior) i.next() ).destroy(); 142 143 contributors.clear(); 144 } 145 146 /************************************************************************ 147 * If the argument is true, pre-install all custom tag handlers 148 * <a href="#customTags">described earlier</a>, 149 * otherwise don't install any of the custom tag handlers. 150 * The no-arg constructor doesn't install any handlers, so 151 * <code>new HTMLPane()</code> and <code>new HTMLPane(false)</code> 152 * are equivalent. 153 * @param installDefaultTags 154 */ 155 public HTMLPane( boolean installDefaultTags ) 156 { this(); 157 if( installDefaultTags ) 158 { addTag( "size" , new SizeHandler() ); 159 addTag( "inputAction", new InputActionHandler(this)); 160 addTag( "inputNumber", new InputNumberHandler() ); 161 addTag( "inputDate" , new InputDateHandler() ); 162 } 163 } 164 165 166 The custom EditorKitThe next order of business is the custom editor kit. For the most part, I just inherit methods from the HTMLEditorKit base class. The critical method in my derived class is the getViewFactory() override (line 178) at the top of the class definition. A view is a widget that represents a tag on the screen, and the view factory creates these widgets. My custom view factory creates widgets to handle custom tags. The other method of interest is the getParser override (line 219). This method returns the HTML parser. I didn’t want to provide a new HTML parser, but I wanted to modify the parser’s behavior to allow for preprocessing. I solved the problem in an, admittedly, awkward way. I wrap the default parser in a version of my own. My wrapper provides the input to the wrapped parser from a custom source rather than the default source. The filterProvider field (declared on line 28 above) points at an instance of FilterFactory (shown in Listing 2).The inputFilter() method is passed a Reader (the default input source) and returns another reader. Typically, the returned reader is a Gang of Four decorator that wraps the original source reader and provides input filtering. That is, the returned reader gets characters from the wrapped reader, processes those characters, and returns processed characters from its own read() method.The filterProvider field is initialized to point at the FilterFactory.NULL_FACTORY object (also in Listing 2) whose factory just returns its argument. You can provide your own FilterFactory, however, which could read from the default stream, modify the input, then return a Reader across the modified input. For example, the NamespaceFilterFactory (in Listing 3) creates a filter that replaces the colon in a namespace-style tab (e.g., <holub:tag ....>) with an underscore so the JEditorPane parser, which doesn’t understand namespaces, can handle it. Install the filter like this: HTMLPane pane = new HTMLPane(); //... pane.filterInput( new NamespaceFilterFactory() ); filterInput() just copies its argument into filterProvider. Here is the custom editor kit code: 167 168 169 /** 170 * The {@link JEditorPane} uses an editor kit to get a factory of 171 * {@link View} objects, each of which is responsible for rendering 172 * an HTML element on the screen. This kit returns a factory that 173 * creates custom views, and it also modifies the behavior of the 174 * underlying {@link Document} slightly. 175 */ 176 public class HTMLPaneEditorKit extends HTMLEditorKit 177 { 178 public ViewFactory getViewFactory() 179 { return new CustomViewFactory(); 180 } 181 182 public Document createDefaultDocument() 183 { HTMLDocument doc = (HTMLDocument)( super.createDefaultDocument() ); 184 185 // <0 for synchronous load. Delays the popup, but allows 186 // tags that set window size, etc. Default value is 4. 187 doc.setAsynchronousLoadPriority(-1); 188 189 // The number of tokens to buffer before displaying them. 190 // A smaller number makes the screen appear a bit more 191 // responsive because the system doesn't pause for a long 192 // time before displaying anything. 193 // 194 // doc.setTokenThreshold(10); 195 // 196 // This is the default value. If it's false, then 197 // custom tags won't be recognized. 198 // 199 // doc.setPreservesUnknownTags( true ); 200 201 return doc; 202 } 203 204 /** Return a parser that wraps the real one. This is the 205 * only convenient way to get a handle to the input 206 * stream that the parser uses: Supply an input-stream 207 * decorator that preprocesses the input before 208 * the parser reads it. 209 * Using a preprocessor at all 210 * is a kludge, but creating a DOM from user 211 * supplied HTML and inserting it into the 212 * HTMLDocument is nasty, and probably slower. 213 * 214 * <code>Parser</code> and <code>ParserCallback</code> 215 * are both inner classes of the {@link HTMLEditorKit} 216 * base class. 217 */ 218 219 protected Parser getParser() 220 { final Parser p = super.getParser(); 221 return new Parser() 222 { public void parse( Reader r, 223 ParserCallback callBack, 224 boolean ignoreCharSet 225 ) throws IOException 226 { p.parse( filterProvider.inputFilter(r), 227 callBack, ignoreCharSet ); 228 } 229 }; 230 } 231 } 232 Listing 2. FilterFactory.java 1 package com.holub.ui.HTML; 2 3 import java.io.Reader; 4 5 /************************************************************************ 6 * A hook for preprocessing input before the HTML parser sees it. The 7 * <code>inputFilter()</code> override is passed the {@link Reader} 8 * that the raw input comes from. It should return a <code>Reader</code> 9 * for the parser to use. The returned <code>Reader</code> 10 * presumably does some processing on the raw input. Register 11 * implementations of this interface with 12 * {@link HTMLPane#filterInput filterInput(...)}. 13 */ 14 15 public interface FilterFactory 16 { Reader inputFilter( final Reader r ); 17 18 public static FilterFactory NULL_FACTORY = 19 new FilterFactory() 20 { public Reader inputFilter(final Reader r) 21 { return r; 22 } 23 }; 24 } Listing 3. NamespaceFilterFactory 1 package com.holub.ui.HTML; 2 3 import java.io.Reader; 4 import java.io.IOException; 5 6 /** A prebuilt {@link FilterFactory} that lets the brain-dead 7 * HTML parser handle namespace-like tags. Looks for tags of the form 8 * <holub:tagname ...> and replaces the colon with an underscore. 9 * You can then provide a custom tag (with the underscore instead of the 10 * colon as the tag name) to handle the tag. 11 * 12 * Install one of these using {@link HTMLPane#filterInput filterInput(...)} 13 * if you want this capability. 14 */ 15 16 public class NamespaceFilterFactory implements FilterFactory 17 { 18 private FilterFactory sourceFilterFactory = FilterFactory.NULL_FACTORY; 19 20 /** Create a NamespaceFilterFactory that gets its input 21 * from the Reader returned from the sourceFilterFactory. 22 * Use this constructor to chain filters. 23 */ 24 public NamespaceFilterFactory( FilterFactory sourceFilterFactory ) 25 { this.sourceFilterFactory = sourceFilterFactory; 26 } 27 28 /** Convenience constructor used when you're not chaining */ 29 public NamespaceFilterFactory() 30 31 /** Returns a Reader that decorates the <code>srcReader</code> 32 * to replace all tags of the form <package:name> with 33 * <package_name>. 34 */ 35 public Reader inputFilter( Reader srcReader ) 36 { 37 final Reader src = (sourceFilterFactory != null) 38 ? sourceFilterFactory.inputFilter( srcReader ) 39 : srcReader 40 ; 41 return new Reader() 42 { private boolean inTag = false; // State must span read() calls. 43 44 public int read( char[] cbuf, int off, int length ) throws IOException 45 { int read = src.read( cbuf, off, length ); 46 for(int i = read; --i >= 0 ; ++off ) 47 { if( cbuf[off] == '<' ) 48 inTag = true; 49 else if( inTag ) 50 { if( cbuf[off] == ':' ) 51 cbuf[off] = '_'; 52 if( cbuf[off] == '>' || Character.isWhitespace(cbuf[off]) ) 53 inTag = false; 54 } 55 } 56 return read; 57 } 58 public void close() throws IOException 59 { src.close(); 60 } 61 }; 62 } 63 } The custom ViewFactory and form handlingThe next inner class is for the custom-view factory, shown in the code below. As with the editor kit, most of the methods are inherited from the base class (in this case javax.swing.text.html.HTMLEditorKit.HTMLFactory). The create() method is passed an Element that indicates the tag for which the factory should create a view. The create() override delegates to the base-class create() for all tags except <input...>, <select...>, <textarea...>, and any tags that aren’t standard HTML (where the instanceof HTML.UnknownTag test succeeds). In the first three cases, a FormView object (which we’ll look at in a moment) is created. In the case of an unknown tag, a ComponentView is created. The java.swing.text.ComponentView class wraps a standard Swing component. In this case, it wraps the component returned from one of the TagHandler objects you provide (details in last month’s column). Here is the custom-view factory code: 233 234 /******************************************************************* 235 * Create views for the various HTML elements. This factory differs from 236 * the standard one in that it can create views that handle the 237 * modifications I've made to EditorKit. For the most part, it 238 * just delegates to its base class. 239 */ 240 241 private final class CustomViewFactory extends HTMLEditorKit.HTMLFactory 242 { 243 // Create views for elements. 244 // Note that the views are not created as the elements 245 // are encountered; rather, they're created more or less 246 // at random as the elements are displayed. Don't do anything here 247 // that depends on the order in which elements appear in the input. 248 // 249 // Also note that undefined start-element tags are not in any way 250 // linked to the matching end-element tag. They two might move 251 // around arbitrarily. 252 253 public View create(Element element) 254 { // dump_Element( element ); 255 256 HTML.Tag kind = (HTML.Tag)( 257 element.getAttributes().getAttribute( 258 javax.swing.text.StyleConstants.NameAttribute) ); 259 260 if( (kind==HTML.Tag.INPUT) || (kind==HTML.Tag.SELECT) 261 || (kind==HTML.Tag.TEXTAREA) ) 262 { 263 // Create special views that understand Forms and 264 // route submit operations to form observers only 265 // if observers are registered. 266 // 267 FormView view = (actionListeners != null) 268 ? new LocalFormView( element ) 269 : (FormView)( super.create(element) ) 270 ; 271 272 String type = (String)( element.getAttributes(). 273 getAttribute(HTML.Attribute.TYPE)); 274 return view; 275 } 276 else if( kind instanceof HTML.UnknownTag ) 277 { // Handling a custom element. End tags are silently ignored. 278 279 if( element.getAttributes(). 280 getAttribute(HTML.Attribute.ENDTAG) == null) 281 { 282 final Component view = doTag( element ); 283 if( view != null ) 284 { return new ComponentView(element) 285 { protected Component createComponent() 286 { return view; 287 } 288 }; 289 } 290 // Else fall through and return default (invisible) View. 291 } 292 } 293 return super.create(element); 294 } 295 } 296 The doTag() method that creates the component is shown below. The method is passed the Element for which we’re providing the view. It then extracts any attributes present in that element and puts them into a Properties object so they are easy to access. Then it tries to find a handler for the tag by looking it up in the tagHandlers Map. (Tags are put into the Map by addTag(), discussed last month, which issues a tagHandlers.put(tagName,customHandler) call.) If doTag() finds a handler for the current tag, it delegates to the handler to get the component to display: 297 298 /******************************************************************* 299 * Handle a request to process a custom tag. If no handler for 300 * a given tag is found, the fact is logged to the com.holub.ui 301 * logger, and the tag is ignored. 302 * 303 * @param element The element that we're handling 304 * 305 * 306 * @return a JComponent to use as the view or <code>null</code> 307 * if there is no view. 308 */ 309 private final Component doTag( Element element ) 310 { 311 if( element == null ) // it does happen! 312 return null; 313 314 String name = element.getName(); 315 if( name == null ) 316 name = "Unknown" ; 317 318 // Extract the attributes and tag name from the Element: 319 320 Properties attributes = new Properties(); 321 AttributeSet set = element.getAttributes(); 322 323 for( Enumeration i = set.getAttributeNames(); i.hasMoreElements(); ) 324 { Object current = i.nextElement (); 325 String attributeName = current.toString (); 326 Object attributeValue = set.getAttribute (current); 327 328 attributes.put 329 ( (current instanceof StyleConstants)? TAG_NAME: attributeName, 330 attributeValue.toString() 331 ); 332 333 } 334 335 // Now look for a handler for the tag. If there isn't one, just 336 // return null, which effectively causes the tag to be ignored. 337 338 TagHandler handler = (TagHandler)( tagHandlers.get(name) ); 339 if( handler == null ) 340 { log.warning( "Couldn't find handler for <" +name+ ">" ); 341 return null; 342 } 343 344 // There is a handler; call it to do the work. Return whatever 345 // component the handler returns. 346 347 JComponent component = handler.handleTag(this,attributes); 348 if( component instanceof TagBehavior ) 349 contributors.add( component ); 350 return ( component ); 351 } 352 Let’s return to the <input...>, <select...>, and <textarea...> elements. These elements are normally handled by a javax.swing.text.html.FormView object. If a form handler is installed, however, then an instance of LocalFormView handles these elements. I show the code below. The main method of interest is the submitData() override, which is called when the user hits the Submit button. The local version submits data to the form handler installed with a prior addActionListener(...) call. (The handleSubmit() method just sends an actionPerformed message to the AWTEventMulticaster that represents the action listeners.)The only other method in the LocalFormView worth mentioning is createComponent(), which creates the components that represent the pieces of the form. The LocalFormView override of this method makes a few cosmetic adjustments to the widgets created by the base-class version. For example, it makes radio buttons transparent so they look reasonable on a non-gray background, and it adjusts the alignment of radio buttons so that they appear centered with respect to surrounding text.Here is the code example: 353 354 355 /******************************************************************* 356 * Special handling for elements that can occur inside forms. 357 */ 358 public final class LocalFormView extends javax.swing.text.html.FormView 359 { 360 public LocalFormView( Element element ) 361 { super(element); 362 } 363 364 /** Chase up through the form hierarchy to find the 365 * <code>&lt;form&gt;</code> tag that encloses the current 366 * <code>&lt;input&gt;</code> tag. There's a similar 367 * method in the base class, but it's private so I can't use it. 368 */ 369 private Element findFormTag() 370 { for(Element e=getElement(); e != null; e=e.getParentElement() ) 371 if(e.getAttributes().getAttribute(StyleConstants.NameAttribute) 372 ==HTML.Tag.FORM ) 373 return e; 374 375 throw new Error("HTMLPane.LocalFormView Can't find <form>"); 376 } 377 378 /** Override the base-class method that actually submits the form 379 * data to process it locally instead if the URL in the action 380 * field matches the "local" URL. 381 */ 382 protected void submitData(String data) 383 { AttributeSet attributes = findFormTag().getAttributes(); 384 String action = 385 (String)attributes.getAttribute(HTML.Attribute.ACTION); 386 String method = 387 (String)attributes.getAttribute(HTML.Attribute.METHOD); 388 389 if( action == null ) action = ""; 390 if( method == null ) method = ""; 391 392 handleSubmit( method.toLowerCase(), action, data ); 393 } 394 395 /** Override the base-class image-submit-button class. Given the tag: 396 * <PRE> 397 * <input type="image" src="grouchoGlasses.gif" 398 * name=groucho value="groucho.pressed"> 399 * </PRE> 400 * The data will hold only two properties: 401 * <PRE> 402 * groucho.y=23 403 * groucho.x=58 404 * </PRE> 405 * Where 23 and 58 are the image-relative positions of the mouse when 406 * the user clicked. (Note that the value= field is ignored.) 407 * Image tags are useful primarily for implementing a Cancel button. 408 * 409 * This method does nothing but chain to the standard submit- 410 * processing code, which can figure out what's going on by 411 * looking at the attribute names. 412 */ 413 protected void imageSubmit(String data) 414 { submitData(data); 415 } 416 417 /** Special processing for the Reset button. I really want to 418 * override the base-class resetForm() method, but it's package- 419 * access restricted to javax.swing.text.html. 420 * I can, however, override the actionPerformed method 421 * that calls resetForm() (Ugh). 422 */ 423 public void actionPerformed( ActionEvent e ) 424 { String type = (String)( getElement().getAttributes(). 425 getAttribute(HTML.Attribute.TYPE)); 426 if( type.equals("reset") ) 427 doReset( ); 428 super.actionPerformed(e); 429 } 430 431 /** Make transparent any standard components used for input. 432 * The default components are all opaque with a gray (0xd0d0d0) 433 * background, which looks awful when you've set the background 434 * color to something else (or set it to an image) in the 435 * <code>&lt;body&gt;</code> tag. Setting opaque-mode 436 * off lets the specified background color show through the 437 * <code>&lt;input&gt;</code> fields. 438 */ 439 protected Component createComponent() 440 { JComponent widget = (JComponent)( super.createComponent() ); 441 442 // The widget can be null for things like type=hidden fields 443 444 if( widget != null ) 445 { 446 if( !(widget instanceof JButton) ) 447 widget.setOpaque(false); 448 449 // Adjust the alignment of everything except multiline text 450 // fields so that the control straddles the text baseline 451 // instead of sitting on it. This adjustment will make 452 // buttons and the text within a single-line text-input 453 // field align vertically with any adjacent text. 454 455 if( !(widget instanceof JScrollPane) ) // <input> 456 { widget.setAlignmentY( BASELINE_ALIGNMENT ); 457 } 458 else 459 { // a JList is a <select>, a JTextArea is a <textarea> 460 Component contained = 461 ((JScrollPane)widget).getViewport().getView(); 462 463 // If it's a select, change the width from the default 464 // (full screen) to a bit wider than the actual contained 465 // text. 466 if( contained instanceof JList ) 467 { widget.setSize( contained.getPreferredSize() ); 468 469 Dimension idealSize = contained.getPreferredSize(); 470 idealSize.width += 20; 471 idealSize.height += 5; 472 473 widget.setMinimumSize ( idealSize ); 474 widget.setMaximumSize ( idealSize ); 475 widget.setPreferredSize( idealSize ); 476 widget.setSize ( idealSize ); 477 } 478 } 479 } 480 return widget; 481 } 482 } 483 The last piece of the form-handling puzzle is the FormActionEvent class. This override of ActionEvent provides a few hooks that allow you to see the user-supplied data on the current form. For the most part, it just exposes inner-class fields. (In general, exposing a field in this way is a bad idea, as I previously discussed in “Why Getters and Setters Are Evil”. The accessor is unavoidable in the current situation, however. The whole raison d’être of the current class is to provide this information to an external object, after all. User interface (UI) widgets, such as the current one, are examples of the procedural boundary-layer exceptions to the no-accessors rule I mentioned in that previous article.) Here is the FormActionEvent code: 484 485 /******************************************************************* 486 * Used by {@link HTMLPane} to pass form-submission information to 487 * any ActionListener objects. 488 * When a form is submitted by the user, an actionPerformed() message 489 * that carries a FormActionEvent is sent to all registered 490 * action listeners. They can use the event object to get the 491 * method and action attributes of the form tag as well as the 492 * set of data provided by the form elements. 493 */ 494 495 public class FormActionEvent extends ActionEvent 496 { private final String method; 497 private final String action; 498 private final Properties data = new Properties(); 499 500 /** 501 * @param method method= attribute to Form tag. 502 * @param action action= attribute to Form tag. 503 * @param data Data provided by standard HTML element. Data 504 * provided by custom tags is appended to this set. 505 */ 506 private FormActionEvent( String method, String action, String data ) 507 { super( HTMLPane.this, 0, "submit" ); 508 this.method = method; 509 this.action = action; 510 try 511 { 512 data = UrlUtil.decodeUrlEncoding(data) + "n" + dataFromContributors(); 513 this.data.load( new ByteArrayInputStream( data.getBytes()) ); 514 } 515 catch( IOException e ) 516 { assert false : ""Impossible" IOException"; 517 } 518 } 519 520 /** Return the method= attribute of the <form> tag */ 521 public String method() { return method; } 522 523 /** Return the action= attribute of the <form> tag */ 524 public String action() { return action; } 525 526 /** Return the a set of properties representing the name=value 527 * pairs that would be sent to the server on form submission. 528 */ 529 public Properties data() { return data; } 530 531 /** Convenience method, works the same as 532 * <code>(HTMLPane)( event.getSource() )</code> 533 */ 534 public HTMLPane source(){ return (HTMLPane)getSource(); } 535 } 536 A few small methodsI hope I explain the next few methods well enough in the comments. I list them below without additional comments. Most of these methods were discussed last month: 537 538 539 //----------------------------------------------------------------- 540 public final void addActionListener( ActionListener listener ) 541 { actionListeners = AWTEventMulticaster.add(actionListeners, listener); 542 } 543 544 public final void removeActionListener( ActionListener listener ) 545 { actionListeners = AWTEventMulticaster.remove(actionListeners, listener); 546 } 547 548 private final void handleSubmit(final String method, final String action, 549 final String data ) 550 { actionListeners.actionPerformed( 551 new FormActionEvent 552 ( method, 553 action, 554 UrlUtil.decodeUrlEncoding(data) 555 + "n" 556 + dataFromContributors() 557 ) 558 ); 559 } 560 561 /*package*/ final void handleInputActionTag( final String name ) 562 { actionListeners.actionPerformed(new FormActionEvent( "", "", "" )); 563 } 564 565 /**...*/ 566 567 public final void addTag(String tag, TagHandler handler) 568 { assert !isStandard_HTML_tag(tag) : 569 "Illegal attempt to redefine standard HTML tag <"+tag+">"; 570 log.info( "Adding custom tag <" + tag + ">" ); 571 tagHandlers.put( tag, handler ); 572 } 573 574 /** Remove the handler for a given tag. Once this call is made, 575 * the tag will be ignored if it's encountered in a newly loaded 576 * page. (A warning is logged if an unexpected tag is 577 * encountered, but this situation is not considered to be a 578 * runtime error.) 579 */ 580 581 public final void removeTag( String tag ) 582 { tagHandlers.remove( tag ); 583 } 584 585 586 /** Notify the JComponents associated with the custom tags that 587 * the user has hit the Reset button. 588 */ 589 590 protected void doReset() 591 { for( Iterator i = contributors.iterator(); i.hasNext(); ) 592 ((TagBehavior) i.next() ).reset(); 593 } 594 595 /** Collect form data from the JComponents associated with the 596 * custom tags and append it to the dataset passed to the 597 * form handlers. This method has been made protected so that 598 * overrides can do some sort of processing or filtering 599 * on the data if they need to. 600 * @return a set of newline-delimited key=value pairs contained 601 * in a single String. 602 */ 603 604 protected String dataFromContributors() 605 { 606 StringBuffer formData = new StringBuffer(); 607 for(Iterator i = contributors.iterator(); i.hasNext(); ) 608 { formData.append( ((TagBehavior) i.next() ).getFormData() ); 609 formData.append("n"); 610 } 611 return formData.toString(); 612 } 613 614 /** Workhorse function used by the assertion at the top of 615 * {@link #addTag}. You can use this method to see 616 * if a tag is available before attempting to register it. 617 * 618 * @return <code>true</code> if the tag argument specifies a standard 619 * HTML tag or one of the internal tags "p-implied" or "content." 620 * Otherwise, return <code>false</code>. 621 */ 622 623 public static final boolean isStandard_HTML_tag(String tag) 624 { tag = tag.toLowerCase(); 625 HTML.Tag[] allTags = HTML.getAllTags(); 626 627 if( tag.equals("p-implied") || tag.equals("content") ) 628 return true; 629 630 for( int i = 0; i < allTags.length; ++i ) 631 if( allTags[i].toString().toLowerCase().equals(tag) ) 632 return true; 633 634 return false; 635 } 636 637 /******************************************************************* 638 * Provide input preprocessing. 639 * Use this method to replace the default input filter 640 * {@link FilterFactory#NULL_FACTORY}. 641 * 642 * @see FilterFactory 643 * @see NamespaceFilterFactory 644 */ 645 646 public void filterInput( FilterFactory provider ) 647 { filterProvider = provider; 648 } 649 650 /** 651 * A convenience method for local testing of a UI that will eventually 652 * be Web based. If you map your home URL to a local directory, 653 * all links to the home URL are automatically replaced by 654 * links to the directory. For example, given 655 * <PRE> 656 * myPane.addHostMapping( "www.holub.com", "file://c:/src/test" ); 657 * </PRE> 658 * an HTML "anchor" that looks like this: 659 * <PRE> 660 * <a href="http://www.holub.com/dir/foo.html"> 661 * </PRE> 662 * is treated as if you had specified: 663 * <PRE> 664 * <a href="file://c:/src/test/dir/foo.html"> 665 * </PRE> 666 * Multiple mappings are supported. That is, if you call this method 667 * more than once, then all the mappings you specify apply. 668 * 669 * The host mappings are shared by <u>all</u> instances of 670 * HtmlFrame, including popups created by target=_blank in a 671 * hyperlink. 672 * 673 * @see #removeHostMapping 674 */ 675 public static final void addHostMapping( String thisHost, 676 String mapsToThisLocation ) 677 { if( hostMap == null ) 678 hostMap = new HashMap(); 679 680 hostMap.put( thisHost, 681 new HostMapping(thisHost, mapsToThisLocation)); 682 } 683 684 /** Remove a host mapping added by a previous call to 685 * {@link #addHostMapping}. 686 * @see #addHostMapping 687 */ 688 public static final void removeHostMapping( String thisHost ) 689 { hostMap.remove(thisHost); 690 } 691 692 /** Map a URL if there's an entry for the host portion in the host map. 693 * @param url a URL specified in terms of the "host" location. 694 * @return a URL for the mapped location. 695 * @see #addHostMapping 696 * @see #removeHostMapping 697 */ 698 public URL map(URL url) 699 { if( hostMap != null ) 700 { HostMapping mapping = (HostMapping) hostMap.get(url.getHost()); 701 if( mapping != null ) 702 url = mapping.map( url ); 703 } 704 return url; 705 } 706 707 /******************************************************************* 708 * This class handles the host-to-local-file mapping mechanics 709 * for {@link #addHostMapping}. The {@link #hostMap} 710 * table is a java.util.Map of objects of this class. 711 */ 712 private static final class HostMapping 713 { private final String remote; 714 private final String local; 715 public HostMapping( String remote, String local ) 716 { this.remote = ".*://" + remote.replaceAll(".", "."); 717 this.local = local ; 718 } 719 public URL map( URL page ) 720 { try 721 { String s = page.toExternalForm(); 722 return new URL( s.replaceFirst(remote, local) ); 723 } 724 catch( MalformedURLException e ) 725 { log.warning( "Couldn't map " + remote + " to " 726 + local + ": " + e.getMessage() ); 727 } 728 return page; 729 } 730 } 731 Hyperlink handlingThe next piece of interesting code is the hyperlink handling. For reasons that are mysterious to me, the out-of-the-box JEditorPane doesn’t handle hyperlinks. The documentation tells you how to add support for them, but it does so by providing an essentially undocumented example you must duplicate on faith. The main thing I added to this code is explicit handling of a target=_blank attribute that causes the page to which you’re linking to appear in a pop-up window. I also code to reject all protocols except for file:/, http:/, and mailto:/. The mysterious (and undocumented) processHTMLFrameHyperlinkEvent() does the real work. I just copied it from the example in the docs, however, and have no idea how it really works. This is the hyperlink handler code: 732 733 /************************************************************************ 734 * This Hyperlink handler replaces the contents of the current pane 735 * with whatever's at the indicated link. This code is copied more or 736 * less verbatim from the Sun documentation. I've also added simple 737 * support for the mailto: protocol. 738 */ 739 740 private final class HyperlinkHandler implements HyperlinkListener 741 { public void hyperlinkUpdate(HyperlinkEvent event) 742 { try 743 { if( event.getEventType() == HyperlinkEvent.EventType.ACTIVATED ) 744 { 745 HTMLPane source = (HTMLPane)event.getSource(); 746 String description = event.getDescription(); 747 Element e = event.getSourceElement(); 748 749 // Get the attributes of the <a ...> tag that got 750 // us here, then extract the target= attribute. If 751 // we find target=_blank, then display the page 752 // in a pop-up window. I assume that the 753 // href references an HTML file, because it wouldn't 754 // make much sense to use target=_blank if it didn't. 755 756 AttributeSet tagAttributes = (AttributeSet) 757 (e.getAttributes().getAttribute(HTML.Tag.A)); 758 759 String target = null; 760 if( tagAttributes != null ) 761 target = (String) tagAttributes.getAttribute( 762 HTML.Attribute.TARGET); 763 764 if( target != null && target.equals("_blank") ) 765 { popupBrowser( event.getURL() ); 766 return; 767 } 768 769 // Handle http: and file: links. If the description 770 // doesn't contain a protocol (there's no ':'), 771 // then assume a "relative" link: 772 773 if( description.startsWith("http:") 774 || description.startsWith("file:") 775 || description.indexOf(":") == -1 776 ) 777 { 778 JEditorPane pane = (JEditorPane) event.getSource(); 779 780 if(event instanceof HTMLFrameHyperlinkEvent) 781 { ((HTMLDocument)(source.getDocument())). 782 processHTMLFrameHyperlinkEvent( 783 (HTMLFrameHyperlinkEvent)event); 784 } 785 else if(!redirect(event.getURL())) 786 { unknownRedirect(event.getDescription()); 787 } 788 } 789 else if( description.startsWith("mailto") ) 790 { if( isWindows() ) 791 Runtime.getRuntime().exec( 792 "cmd.exe /c start " + description); 793 else 794 unknownRedirect(event.getDescription()); 795 } 796 else 797 { unknownRedirect( event.getDescription() ); 798 } 799 } 800 } 801 catch( Exception e ) 802 { log.warning 803 ( "Unexpected exception caught while processing hyperlink: " 804 + e.toString() + "n" 805 + Log.stackTraceAsString( e ) 806 ); 807 } 808 } 809 810 /** Used for anchor tags that include target=_blank attributes. Pop up 811 * a new instance of an HTMLPanel in a subwindow, displaying the page 812 * whose URL is specified in the "description" argument. 813 */ 814 private final void popupBrowser( URL target ) 815 throws MalformedURLException 816 { JFrame popupFrame = new JFrame(); 817 HTMLPane popup = new HTMLPane(); 818 popupFrame.getContentPane(). add( new JScrollPane(popup) ); 819 820 popup.redirect(target); 821 Dimension size = popup.getPreferredSize(); 822 Point location = getLocationOnScreen(); 823 popupFrame.setBounds( location.x + 10, 824 location.y + 10, 825 size.width, 826 size.height ); 827 popupFrame.show(); 828 } 829 830 /** Mailto support is OS specific, so we need to know the OS. 831 */ 832 private final boolean isWindows() 833 { return( System.getProperty("os.name").toLowerCase(). 834 indexOf("windows") != -1); 835 } 836 } 837 838 839 /******************************************************************* 840 * Handles hyperlinks for the http:// or file:// protocols. 841 * Not all files are loaded, and a name-based algorithm 842 * is used to determine what gets loaded and what doesn't 843 * get loaded. 844 * All requests that are not handled here (i.e., for a protocol 845 * other than http: or file:, or for a file with an extension 846 * not on the list, below) are routed to 847 * {@link #unknownRedirect(String)} 848 * for processing. 849 * Overload the current method to change the way that the 850 * http: and file: protocols are processed. 851 * Overload {@link #unknownRedirect(String)} to add support 852 * for other protocols. 853 * 854 * Any host mappings specified to 855 * {@link #addHostMapping} are processed first. 856 * Then, those URLs that end in 857 * one of the following extensions are handled. 858 * <table class="legacyTable" border=0 cellspacing=0 cellpadding=0> 859 * <tr><td><code>.shtml</code></td><td> HTML file </td></tr> 860 * <tr><td><code>.html </code></td><td> HTML file </td></tr> 861 * <tr><td><code>.htm </code></td><td> HTML file </td></tr> 862 * <tr><td><code>.pl </code></td><td> Perl script </td></tr> 863 * <tr><td><code>.jsp </code></td><td> Java Server Page </td></tr> 864 * <tr><td><code>.asp </code></td><td> Active Server Page </td></tr> 865 * <tr><td><code>.php </code></td><td> PHP script </td></tr> 866 * <tr><td><code>.py </code></td><td> Python Script </td></tr> 867 * </table> 868 * 869 * All URLs whose paths do not have a '.' in the string that follows the 870 * right-most slash (including directory specifications, which should be 871 * terminated by a slash) are assumed to reference an implicit index.html 872 * file. Similarly, a URL that specifies a host or directory, but no file 873 * (e.g. "http://www.holub.com" or "http://www.holub.com/directory/), 874 * is loaded. 875 * 876 * N.B.: A relative file that doesn't have an extension (such as 877 * <code>&lt;a href=filename&gt;</code> is not recognized as 878 * an HTML file, so is not processed. 879 * 880 * You can override this method if you need to change this default 881 * behavior. This method is called from the Swing event thread, so 882 * it's safe for your override to use 883 * {@link JEditorPane#setPage(URL)} 884 * 885 * @param page the target page. Must be a file: or http: URL. 886 * @return true if the page was handled. If you return false, 887 * {@link #unknownRedirect(String)} is given a chance 888 * to process it. 889 */ 890 891 public boolean redirect( URL page ) 892 { assert( page.getProtocol().equals("http") 893 || page.getProtocol().equals("file") ); 894 try 895 { String file = page.getFile(); 896 if( !(file.length()==0 || htmlExtensions.matcher(file).matches()) ) 897 return false; 898 setPage( page ); // Local version of setPage handles host mapping. 899 } 900 catch (Throwable t) 901 { String message = "HTMLPane couldn't open hyperlink: " 902 + t.getMessage(); 903 log.warning( message ); 904 JOptionPane.showMessageDialog( 905 HTMLPane.this, 906 message, 907 "401 Error", 908 JOptionPane.WARNING_MESSAGE ); 909 } 910 return true; 911 } 912 913 /** A regular expression that identifies all file extensions 914 * recognized by {@link #redirect(URL)} 915 * as an HTML file. The same expression also recognizes 916 * directories (that end in a slash) and all filenames 917 * that don't have an extension. 918 */ 919 private static final Pattern htmlExtensions = 920 Pattern.compile( "(.*.(html|htm|pl|jsp|asp|php|py|shtml)([?#].*)?$)" 921 + "|(.*/[^./]+([?#].*)?$)" 922 + "|(.*/([?#].*)?$)", 923 Pattern.CASE_INSENSITIVE ); 924 925 /** Handles all hyperlink protocols that aren't recognized by 926 * {@link #redirect redirect(...)}. 927 * Is also called if a mailto: protocol is specified and 928 * we're not running under Windows. This implementation just logs 929 * a warning to "com.holub.ui" and pops up a warning-style dialog box 930 * indicating that the protocol isn't supported. You can override 931 * this method to support protocols other than file:// and http:// 932 */ 933 public void unknownRedirect( String request ) 934 { log.warning("HTMLPane: Protocol or file type not supported (" 935 + request + ")" ); 936 JOptionPane.showMessageDialog 937 ( HTMLPane.this, 938 "Protocol or file type not supported (" + request + ")", 939 "Link Error", 940 JOptionPane.WARNING_MESSAGE 941 ); 942 } 943 944 Page-loading overridesThe remainder of the class definition, shown below, provides a few overrides of the base-class methods that load the first page. For the most part, these overrides just shut down currently displayed pages in an orderly way, then chain to the base-class method to load the new page: 945 946 /******************************************************************* 947 * Overrides {@link JEditorPane#setText(String)} 948 * to do general housekeeping. 949 * @see #setPage(URL) 950 * @see #setPage(String) 951 */ 952 public final void setText(String text) 953 { handlePageShutdown(); 954 super.setText(text); 955 } 956 957 /** Overrides {@link JEditorPane#setPage(URL)} to 958 * map hosts, and do general housekeeping. 959 * @see #setText(String) 960 * @see #setPage(String) 961 */ 962 public final void setPage(URL url) throws IOException 963 { handlePageShutdown(); 964 super.setPage( map(url) ); 965 } 966 967 /** This version of setPage is disabled in an HTMLPane. You can 968 * use setPage(new URL(...)); if all you have is a string. 969 * @see #setText(String) 970 * @see #setPage(URL) 971 * @throws UnsupportedOperationException always 972 */ 973 public final void setPage(String location) 974 { throw new UnsupportedOperationException( 975 "setPage(String) not supported by HTMLPane" ); 976 } 977 978 /** Read is disabled in an HTMLPane. 979 * @throws UnsupportedOperationException always 980 */ 981 public void read(InputStream in, Object desc) throws IOException 982 { throw new UnsupportedOperationException( 983 "read() not supported by HTMLPane" ); 984 } 985 986 /** A version of {@link JEditorPane#setPage(URL)} 987 * that can be called safely from somewhere other than a Swing 988 * event handler. Note that your form handlers <i>are</i> 989 * being called from an event handler. 990 */ 991 public void setPageAsynchronously(final URL page) throws IOException 992 { SwingUtilities.invokeLater 993 ( new Runnable() 994 { public void run() 995 { try 996 { setPage(page); 997 } 998 catch(IOException e) 999 { log.warning("HTMLPane: setPage() failed on Event Thread"); 1000 } 1001 } 1002 } 1003 ); 1004 } 1005 I left out…The HTMLPane.java in the source distribution also contains an elaborate unit-test class, which I have not shown.HTMLPane.java also uses a few helper classes:URLUtil contains a few static methods that do URL manipulation.Log is another utility that makes it easier to set up Java’s logging facility.AncestorAdapter is an implementation of AncestorListener whose methods do nothing. (I’m not sure why this one was left out, since there are similar adapters for all the other listeners.)Again, I put the sources for these classes into the source distribution on my Website, but haven’t included them here.The discussion of Swing’s use of Factory Method at the beginning of this article is excerpted from my forthcoming book Holub on Patterns: Learning Design Patterns by Looking at Code (Apress). However, the HTMLPane itself is not covered in the book.Allen Holub has worked in the computer industry since 1979. He currently works as a consultant, helping companies not squander money on software by providing advice to executives, training, and design-and-programming services. He’s authored eight books, including Taming Java Threads (Apress, 2000) and Compiler Design in C (Pearson Higher Education, 1990), and teaches regularly for the University of California Berkeley Extension. Find more information on his Website (http://www.holub.com). JavaSoftware DevelopmentDesign PatternsHTML