Create client-side user interfaces in HTML, Part 2

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  }
Figure 1. Overlapping factory methods in JEditorPane

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.

1 2 3 4 5 Page 1