by Allen Holub

Create client-side user interfaces in HTML, Part 2

how-to
Nov 14, 200338 mins

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:

Note: You can download this article’s associated source code from my Website.

Extend JEditorPane with the Factory Method pattern

The 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 details

Let’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 constructors

Next 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 EditorKit

The 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   *  &lt;holub:tagname ...&gt; 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 &lt;package:name&gt; with
  33       *  &lt;package_name&gt;.
  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 handling

The 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>&amp;lt;form&amp;gt;</code> tag that encloses the current
  366           *   <code>&amp;lt;input&amp;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           *  &lt;input type="image" src="grouchoGlasses.gif"
  398           *                 name=groucho value="groucho.pressed"&gt;
  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>&amp;lt;body&amp;gt;</code> tag. Setting opaque-mode 436 * off lets the specified background color show through the 437 * <code>&amp;lt;input&amp;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 &lt;form&gt; tag */
  521          public String     method()  { return method; }
  522
  523          /** Return the action= attribute of the &lt;form&gt; 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 methods

I 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       *  &lt;a href="http://www.holub.com/dir/foo.html"&gt;
  661       *  </PRE>
  662       *  is treated as if you had specified:
  663       *  <PRE>
  664       *  &lt;a href="file://c:/src/test/dir/foo.html"&gt;
  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

The 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>&amp;lt;a href=filename&amp;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 overrides

The 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).