Make the JEditorPane useful This month I’m going back to programming for a while. I need a rest from the weirdness on the Talkback discussion in last month’s column. I do intend to write more about theory issues in the future, however, but not for the next couple months.I do need to make one clarification from last month’s column. Many people interpreted my comments about user interfaces as advocating heavyweight objects with billions of rendering methods in them. That’s not what I had in mind. There are numerous viable ways to create user interfaces (UIs) without exposing implementation details. The Gang of Four Builder and Visitor patterns immediately come to mind. A simple drawYourself() method obviously can’t work on anything but the most simple objects, and having 50 drawYourselfInThisFormat() and drawYourselfInThatFormat() methods is a nonsensical recipe for unmanageable code. Many people think I’m advocating that approach, however, so I apologize if I gave that impression.Since I’ve seen a lot of misunderstanding regarding the UI issue, I’ll plan on showing you a few implementations of object-oriented (OO) UI-building approaches in future columns. I presented one such solution in JavaWorld a few years ago (see Resources), but I’ve built better systems in the intervening years. This current column presents a piece of one of these UI systems: an infrastructure class I’ve used in building client-side UIs in an OO way. It is not in and of itself a solution to the UI problem, but it’s a useful building block. Since the code samples are rather large, I’ll break up the presentation into two chunks. This month is documentation and application code; next month is the source code.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)Using HTML on the client sideHTML is a wonderful thing. It lets you lay out complicated user interfaces with a minimum of fuss; it does a great job of separating UI structure and layout from business logic; it’s easy to write; and it’s easy to maintain. Abstract Window Toolkit (AWT)/Swing layout is, in contrast, annoyingly difficult to use. You must modify (and recompile) code to change the look of your screens, and the code for a trivial layout is itself nontrivial, extending to many pages. Wouldn’t it be nice if you could specify your entire client-side user interface in HTML? (I know that some of you will answer the preceding question with a rousing “No, it wouldn’t be nice!” Many argue that HTML and a good user experience are mutually exclusive concepts, since HTML forces your user interface into “infinite cascading dialog box” mode. On the other hand, a lot of applications can leverage HTML effectively in at least some part of the user interface—for tabular reports if nothing else. You can’t throw out the baby with the bath water.)Swing’s JEditorPane class, at first, seems to be an answer to the HTML-layout problem. It does understand HTML input after a fashion. For example, the following code pops up a frame that displays some simple HTML text:JFrame main_frame = new JFrame(); JEditorPane pane = new JEditorPane(); pane.setContentType ( "text/html" ); pane.setEditable ( false ); pane.setText ( "<html>" + "<head>" + "</head>" + "<body>" + "<center><b>Hello</b> <i>World</i></center>" + "</body>" + "</html>" ); main_frame.setContentPane(pane); main_frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); main_frame.pack(); main_frame.show(); I say “after a fashion” because JEditorPane is not particularly good at handling complex HTML. It doesn’t do a great job with nested tables, and it doesn’t do Cascading Style Sheets (CSS) very well. (I’ve been told that many of these problems will be fixed in the Java 1.5 release next year, but for the time being, we all have to put up with them.) Finally, JEditorPane doesn’t do a particularly good job of laying out things like radio buttons. They aren’t aligned properly with the text baseline, and they always display a gray background, so they don’t work well if you change the page’s background color (with a style on the <body...> tag, for example). All these flaws are annoying, but the show-stopper problem with JEditorPane is that it works as a text control, not as a layout facility. You can specify an HTML <form...> in the input, for example, but the form is submitted to a Web server when you hit the Submit button. To be useful for specifying a client-side UI, you want the form data to go back to the program that displayed the form, not to some remote server. You also need a convenient way to add custom tags for nonstandard input or display purposes or to provide place holders for standard Swing JComponents you want to use on the form. (JEditorPane lets you do this, but the mechanism is far from convenient.) Finally, you need to be handle things like a Cancel button, which doesn’t exist in HTML.Fortunately, the most egregious of the foregoing problems can be solved using customization facilities built into JEditorPane itself. Fixing these problems involves a certain amount of compromise, though. For example, you could handle the Cancel-button problem by implementing a JavaScript interpreter and supporting the onclick attribute, but that’s an awful lot of work. Similarly, providing true custom-tag support (where you can process everything that comes between a start and end tag) is very difficult to do with the existing parser. You could replace the JEditorPane‘s parser with a better one, but that’s a lot of work too. I opted for simpler solutions that did the job. I put enough functionality in my class that I could use it to build a user interface for the program I was writing, but didn’t provide a “perfect” solution. The problem I was solving was: provide a way to specify a user interface in HTML. I was not solving the problem: provide a way to display all possible HTML in a client-side application. The HTMLPane class I present in this article solves the specify-a-UI-in-HTML problem nicely.Using the HTMLPaneMy client-side-only HTML-input class, HTMLPane, is a JEditorPane derivative that fixes the problems discussed earlier. Listing 1 shows how to use an HTMLPane. I created a simple JDialog derivative called HtmlDialog in which you can specify dialog-box layout as HTML. The HtmlDialog is a trivial example of the Façade design pattern. It just does the rote work necessary to put an HTMLPane into a dialog box and display it. The HtmlDialog.Test class (Listing 1, line 134) provides a simple example of how to use the HtmlDialog. It creates a mostly empty main frame (owner). Using code like the snippet reproduced below, main() creates an HtmlDialog object whose contents are specified in the CLASSPATH-relative file com/holub/ui/HTML/test/okay.html (Listing 2). The string "Test HtmlDialog" appears in the title bar. Finally, main() pops up the dialog by calling d.popup(), which won’t return until the user shuts down the dialog:// Display the okay.html file in a dialog box that has // the title "Test HtmlDialog". // HtmlDialog dialog = new HtmlDialog( owning_frame, "com/holub/ui/HTML/test/okay.html", "Test HtmlDialog"); // Pop up the dialog and wait for the user to dismiss it. // dialog.popup(); // Print the "form" data that the user typed. // System.out.println ( "hidden=" + dialog.data().getProperty("hidden") + "user-input" + dialog.data().getProperty("user-input") ); Form data (the text the user typed into an <input ... name="x" ...> element or equivalent), is available via the HtmlDialog‘s data() method, which returns a java.util.Properties object that holds key/value pairs representing the form data. The above call to dialog.data().getProperty("hidden") returns the string "hidden-field data". The dialog.data().getProperty("user-input") call returns whatever the user typed into the input field.Most of the work involved in instantiating the encapsulated HTMLPane happens in the HtmlDialog constructor (Listing 1, line 46). The constructor first sets up an ActionListener that handles the Submit button on the form. This Observer shuts down the current dialog box and copies any form data from the HTMLPane to the data instance variable. The constructor then gets the input file from the CLASSPATH, and then loads the HTML into the HTMLPane using setText(). (There’s also a setPage(URL) method, but you would need a URL for the absolute path to the file if you used it. I wanted the HTML filename to be CLASSPATH relative.) Cancel processing is handled in popup() (line 121), which assumes that a Cancel button was pressed if a Cancel key exists in the submitted form data. (More on how that data gets into the Properties object in a moment.)Listing 1. HtmlDialog.java 1 package com.holub.ui.HTML; 2 3 import javax.swing.*; 4 import java.awt.*; 5 import java.awt.event.*; 6 import java.net.URL; 7 import java.util.Properties; 8 import java.io.IOException; 9 10 /** 11 * A model dialog that holds an {@link HTMLPane}. The dialog shuts 12 * down when the user hits a button created by either an 13 * <input type=submit> or (HTMLPane-specific) 14 * <inputAction> tag. 15 * Display the dialog by calling <code>show()</code>. 16 * For example: 17 * <PRE> 18 * HtmlDialog d = new HtmlDialog( owner, 19 * "com/holub/ui/HTML/test/okay.html", 20 * "Test HtmlDialog" ); 21 * if( d.popup() ) // Dialog not cancelled. 22 * d.data().list( System.out ); 23 * </PRE> 24 * The default custom tags 25 * are all preinstalled in the underlying 26 * {@link HTMLPane}. 27 */ 28 29 public class HtmlDialog extends JDialog 30 { 31 private Properties data = null; 32 33 private final HTMLPane pane = new HTMLPane(true); 34 35 /** Create and initialize a modal HTMLPane. 36 * @param owner The Frame that "owns" this dialog (the parent). 37 * @param htmlPath The CLASSPATH-relative path to the .html file 38 * that holds the contents. For example, if CLASSPATH 39 * contains the directory /usr/src, then get the 40 * file in /usr/src/html/test.html by specifying 41 * <code>"html/test.html"</code> (no leading slash). 42 * @param title Dialog-box title-bar contents. 43 * @throws IOException if it can't open the file on the htmlPath; 44 */ 45 46 public HtmlDialog( Frame owner, String htmlPath, String title ) 47 throws IOException 48 { super( owner, title, true /*it's modal*/ ); 49 50 // Set up an action listener that handles the form 51 // submission. [actionPerformed() is called when the 52 // user hits Submit or Cancel. 53 54 pane.addActionListener 55 ( new ActionListener() 56 { public void actionPerformed(ActionEvent event ) 57 { HtmlDialog.this.setVisible(false); 58 HtmlDialog.this.dispose(); 59 data = ((HTMLPane.FormActionEvent)event).data(); 60 } 61 } 62 ); 63 64 // Load the input file from a CLASSPATH-relative directory 65 // specified in the HTMLPath argument, then import it into 66 // the HtmlDialog for display. 67 68 URL loadFrom = getClass().getClassLoader().getResource(htmlPath); 69 if( loadFrom == null ) 70 { throw new IOException("Can't find $CLASSPATH/" + htmlPath ); 71 } 72 73 pane.setPage( loadFrom ); 74 getContentPane().add( pane ); 75 pack(); 76 } 77 78 /** Get a {@link java.util.Properties} object that holds the 79 * data provided by the <input> elements on the form 80 * (or equivalent). The key is the element "name," the value 81 * is either the value specified in the attribute or the 82 * data the user typed. 83 * 84 * @return the form data 85 * @throw java.lang.IllegalStateException if you try to call this 86 * method before the user submits the form. This can only 87 * happen if <code>data()</code> is called from a thread 88 * other than the one that issued the <code>popup()</code> 89 * request (which blocks). 90 */ 91 92 public Properties data() 93 { if( data == null ) 94 throw new java.lang.IllegalStateException( 95 "Tried to access data before form was submitted"); 96 return data; 97 } 98 99 /** Add custom-tag processing to this dialog. Passes arguments through 100 * to a contained HTMLPane's {@link HTMLPane#addTag addTag(...)} method. 101 * A few out-of-the-box custom tags are already implemented for you 102 * (see {#link HTMLPane}). 103 * 104 * @param tag 105 * @param handler 106 */ 107 public void addTag( String tag, TagHandler handler ) 108 { pane.addTag(tag,handler); 109 } 110 111 /** Works just like {@link JFrame#show}, but returns true if the 112 * dialog was closed with a normal close button. Returns false if 113 * the user submitted the dialog by pressing a button specified with 114 * the tag 115 * <PRE> 116 * <inputAction name=cancel ... > 117 * </PRE> 118 * (<a href=HTMLPane.java#inputAction>See</a>) 119 * or just closed the dialog by clicking the "close" icon. 120 */ 121 public boolean popup() 122 { show(); 123 if( data == null ) // Dialog aborted with "close" icon. 124 return false; // Treat it like a cancel. 125 126 String cancel = data.getProperty("cancel"); 127 if( cancel == null ) // no cancel button 128 return true; 129 130 return !cancel.equals("true"); // True ==> cancel button pressed, 131 // so return false. 132 } 133 134 static private class Test 135 { 136 public static void main( String[] args ) throws Exception 137 { 138 JFrame owner = new JFrame(); 139 owner.getContentPane().add( new JLabel("Parent Frame") ); 140 owner.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); 141 owner.pack(); 142 owner.show(); 143 144 HtmlDialog d = new HtmlDialog(owner, 145 "com/holub/ui/HTML/test/okay.html", "Test HtmlDialog"); 146 147 if( d.popup() ) 148 System.out.println("OK Pressed"); 149 else 150 System.out.println("Cancel Pressed"); 151 152 d.data().list( System.out ); 153 } 154 } 155 } Listing 2. okay.html 1 <html> 2 <head> 3 <title>Titles are Ignored</title> 4 </head> 5 <body> 6 7 <form> 8 <input type="hidden" name="hidden" value="hidden-field data"> 9 Type something: <input type="text" size="20" name="user-input"><br> 10 <input type="submit" value="OK"> 11 </form> 12 <br> 13 <br> 14 </body> 15 </html> The detailsIf we look at HTMLPane a bit more depth, HTMLPane augments JEditorPane in several ways: An HTMLPane logs nonfatal errors to the com.holub.ui logger rather than throwing exceptions. You can hook into these messages by creating a logger (described below).I improved the appearance of form elements:Radio buttons and text-input fields are now aligned properly with the surrounding text.The list created by a select element is now only a little wider than the contained text (instead of taking up the full screen).Radio buttons and checkboxes have transparent backgrounds so they don’t look weird against background textures and colors that aren’t dark gray (bgcolor="#d0d0d0").You can add custom tags (and specify handlers for them), so you add simplistic JavaServer Pages (JSP)-style behavior to your forms. This feature is good primarily for specifying a tag in the HTML input that’s replaced by a Swing component of your devising on the displayed form.You can specify a local form-data handler invoked in response to both submit-style and cancel-style operations. Clicking a Submit button causes the HTMLPane to execute a chunk of code you provide rather than sending the form data to a remote server.Standard http: and file: hyperlinks and relative URLs are handled, though other protocols (e.g., ftp:) are not supported. If a user clicks on the hyperlink, the contents of the HTMLPane are replaced by whatever the associated URL references.Only those hyperlinks that reference files that end in one of the following extensions are loaded:.shtmlHTML file.htmlHTML file.htmHTML file.plPerl script.jspJavaServer Page.aspActive Server Page.phpPHP script.pyPython script My intent is to link only to HTML files or code that generates HTML.All URLs whose paths do not have a '.' in the string that follows the rightmost slash are assumed to reference an implicit index.html file, so the string index.html is appended. Similarly, index.html appends to URLs that don’t specify any directory, or which end in a slash (e.g., http://www.holub.com or http://www.holub.com/directory/).A relative file that doesn’t have an extension (such as is not recognized as an HTML file, so it is not processed. (You can overload a method in HTMLPane to change any and all of this behavior.) mailto: hyperlinks are supported on the Windows platform only. Attempts to use mailto: on other platforms result in a logged warning, but are otherwise silently absorbed.The <a target="_blank" rel="noopener"> attribute correctly causes the page to pop up in its own window. The window is a pack()ed frame that holds an HTMLPane, so your custom-tag handlers will work in the popup.HTMLPane supports a host-mapping feature that translates the “host” part of a URL into an arbitrary map-to URL. All hyperlinks aimed at the host end up at the map-to location. Given:HTMLPane my_pane; //... my_pane.add_host_mapping( "www.holub.com", "file://c:/src/test" ); an HTML anchor that looks like this:<a href="http://www.holub.com/dir/foo.html"> is treated as if you had specified:<a href="file://c:/src/test/dir/foo.html"> If you call add_host_mapping(..) more than once, all the mappings you specify are applied. All HTMLFrame instances share the host mappings, including popups created by a <a target="_blank" rel="noopener"> hyperlink. Form processingNormally, when an HTML form is submitted, the JEditorPane tries to execute an HTTP POST or GET operation on a remote server, passing the server the data associated with the form elements as name=value pairs.HTMLPane changes the default form-processing behavior so that a form is submitted to the current program rather than to some server on the Net. Just add a java.awt.event.ActionListener to the HTMLPane by calling addActionListener(...). The listener’s actionPerformed() method is called when the user hits the Submit button.The associated ActionEvent object is actually an HTMLPane.FormActionEvent class instance, and you can get the submit data directly from this event object. Here’s an example of a simple form-submission handler that prints all form data on standard output and then displays a “success page” in the frame that used to hold the form:pane.addActionListener ( new ActionListener() { public void actionPerformed( ActionEvent event ) { FormActionEvent act = (FormActionEvent)event; // Print the "method=" and "action=" arguments of the // original <form...> tag. System.out.println("n"+ act.getActionCommand() ); System.out.println("t"+"method=" + act.method() ); System.out.println("t"+"action=" + act.action() ); // Print all the data associated with an <input...> // element or equivalent. act.data().list( System.out ); System.out.println(""); // Replace the form that was just submitted // with the the "success page". try { act.source().setPage ( new URL( "file://c:/src/com/holub/ui/HTML/test/submit.html") ); } catch( Exception e ) { e.printStackTrace(); } System.out.println(""); } } ); Once you’ve added any form handlers, all form submissions will go to them rather than being posted to the target URL. Your handler can relay the data to the Web if it likes, but it must do so if you want that behavior. The listeners are all passed the same FormEvent object.Multiple listeners facilitate an object-oriented approach to user-interface construction that I mentioned briefly last month. A control object builds a composite UI by asking several business objects to each contribute some of the displayed HTML. This contribution could contain <input...> tags or equivalent if the object needs to get information from the user. All of the contributing objects can listen for the “submit,” and when the submit happens, each object parses out of the resulting form data only the information it’s interested in. In other words, if an object contributes some element to the HTML, it can get back the value the user typed for that element. This way, neither the HTMLPane itself, the other contributors to the form, nor the control object need to know how the contributing object is implemented. I’ll expand on this architecture in future columns, but it’s a variant on the Presentation/Abstraction/Control architecture discussed in Frank Buschmann, et al’s Pattern-Oriented Software Architecture (see Resources). Custom tagsYou can define custom tags you can use in your HTML input in a manner similar to a custom JSP tag. Set up a new tag by calling some_pane.add_tag(...), passing it an object that identifies a tag handler that’s activated when the tag is encountered. (More in a moment.)Because JEditorPane underlies the current class, you can’t define a namespace-style custom tag: a name like <holub:my_tag ...> (that contains a colon) isn’t recognized. You can use underscores, but that’s a kludge.Also, the current implementation of custom tags does not permit the element to have content. The problem is that the default parser does not pair non-HTML tags. That is, <foo> and </foo> are treated as completely independent tags with no connection between them. The only solution is to effectively rewrite the parser, but that’s a lot of work for not much gain. Several pre-built custom tags are provided. Support for these pre-built tags are installed automatically if you use the HTMLPane(boolean) constructor. Otherwise, add support for a pre-built tag by sending one of the following messages to the HTMLPane object:pane.add_tag( "size" , new Size_handler() ); pane.add_tag( "input_action", new Input_action_handler(this)); pane.add_tag( "input_number", new Input_number_handler() ); pane.add_tag( "input_date" , new Input_date_handler () ); The tags work as follows:<size height=400 width=400> This specifies the size of the pane in pixels.<input_action name="myName" value="label on button"> This tag inserts a Submit-style button that causes actionPerformed(...) messages to be sent to all registered ActionListener objects (as if the Submit button had been pressed). With the exception of the form data supplied by other custom tags, none of the normal form data is available in the FormActionEvent object passed to the listeners, so this tag is useful primarily for a Cancel button. The method() and action() methods of the FormActionEvent return empty strings. The FormActionEvent object’s data() method returns a Properties object that holds a name/value pair for this button. (The name is specified with the name= attribute; the value is the string "true").<input_number name="fred" value="0.0" min="0" max="100" precision="2" size="20"> This is like an <input type=text> tag, but it inputs a numeric value in the range min <= N <= max, with up to precision digits permitted to the right of the decimal point (precision can be zero). If the user attempts to enter an out-of-range value, the control pops up a tooltip-like window that tells the user what’s wrong. It also supports a normal tooltip that tells the user the acceptable range of values. The optional size attribute is the width of the field in columns.<input_date name="fred" value="10/15/03" size="20"> This is a localized (i.e., internationalized) date-input field that does data validation when it loses focus or you hit Enter. Most common date formats are recognized, and a popup dialog (the calendar widget I covered back in July) lets you choose dates from a graphical calendar. The initial value, if present, must specify a date in one of the standard formats. (An empty string isn’t permitted.) If no value= attribute is specified, today’s date is used as the initial value. The optional size attribute is the width of the field in columns. (An approximation is used to size the control.)If the pre-built custom tags aren’t suitable, you can create your own custom tag by implementing the TagHandler interface (Listing 3) and then registering your implementation by calling addTag(), in the same way that the pre-built handlers were registered, above. In the following example, I create and register a handler that just prints all the attributes for a custom <myTag some-attribute="hello"> element. The attributes argument references a Properties object that contains all of the attributes of a tag: pane.addTag ( "myTag", new TagHandler() { public JComponent handleTag(HTMLPane source, Properties attributes ) { attributes.list( System.out ); return null; } } ); Given the HTML input <myTag value="hello world">, the string hello world is printed to System.out when the tag is encountered in the input. (I know that’s not very useful, but bear with me.) You can access the attribute by name inside handleTag(...) by using:String contents = attributes.getProperty("value"); The handler’s source argument references the HTMLPane object in which the custom tag will be displayed. You can use it to create a tag that changes the physical appearance of the HTMLPane. The following example shows how the pre-built <size height=300 width=200> tag is implemented:pane.addTag ( "size", new TagHandler() { public JComponent handleTag(HTMLPane source, Properties attributes) { source.setPreferredSize ( new Dimension ( Integer.parseInt(attributes.getProperty("width")), Integer.parseInt(attributes.getProperty("height")) ) ); return null; } } ); Neither of the foregoing examples show up in the HTMLPane. That is, if the custom-tag handler returns null, then the tag is effectively removed from the input after the handler runs. Define a custom tag that’s a place holder for a JComponent simply by returning that JComponent from handleTag(...).The following code creates a <hello-button> tag, which displays as a button, and prints hello world on the console when it’s clicked:Button helloButton = new JButton( "Hello World" ); hello.addActionListener ( new ActionListener() { public actionPerformed(ActionEvent e) { System.out.println("Hello World"); } } ); pane.addTag ( "hello-button", new TagHandler() { public JComponent handleTag(HTMLPane source, Properties attributes) { return helloButton; } } ); Finally, a custom tag can also contribute form data sent to your program when a user presses the Submit button. The basic strategy is to return a JComponent that implements the TagBehavior interface (Listing 4).Listing 5 demonstrates how to use the interface. A ContributingText object appears on the form as a text control (a JTextField). Whatever the user types will appear in the form-data Properties object you get from the FormActionEvent‘s data() method when the user hits the Submit button, as I described earlier.The name argument to the ContributingText constructor specifies the key value. The initialValue argument is used to populate the text field, but whatever string is represented in the text field is the value associated with that key when you get the form data.The reset() method is called when the user hits the Reset button (put on the form with a <input type="reset"> element). In Listing 5, it restores the text control to its initial value.The destroy method is called when the form shuts down. In the current example, it does nothing, but you can use it do things like store the data the user entered in a database.Finally, the getFormData() method is called by the HTMLPane as it assembles the Properties object that will be returned with the ActionEvent. This method should return a string of the form key=value.You then integrate your component with the HTMLPanel using addTag(). The following example creates a <text> tag that takes name and value attributes. The handler constructs (and returns) a ContributingText object, initialized with the attributes’ values. Here’s the code:// Create the tag <text name="xxx" value="hello world">. // The tag is replaced by a ContributingText object, initialized // with the string specified in the value attribute. The // user may modify this string, and whatever string is in the // text control when the Submit button is pressed is returned // in the form data. pane.addTag ( "text", new TagHandler() { public JComponent handleTag(HTMLPane source, Properties attributes) { return new ContributingText( attributes.getProperty("name"), attributes.getProperty("value") ); } } ); Just so you can see a complete example, I’ve included (in Listing 6) the sources for the InputActionHandler class used to implement the <input_action ...> tag described earlier.Listing 3. TagHandler.java 1 package com.holub.ui.HTML; 2 3 import java.util.Properties; 4 5 import javax.swing.JComponent; 6 import java.util.Properties; 7 8 /** Define a custom tag handler. See the documentation {@link HTMLPane} 9 * for an in-depth explanation of how to use this interface. 10 */ 11 12 public interface TagHandler 13 { 14 /**...*/ 15 JComponent handleTag( HTMLPane source, Properties attributes); 16 } Listing 4. TagBehavior.java 1 package com.holub.ui.HTML; 2 3 /** This interface provides the wherewithal for a <code>JComponent</code> 4 * to act like an HTML <input> tag in an {@link HTMLPane}. 5 * 6 * Note that the methods of this interface are lower level 7 * than those of the Provider. For example, the <code>destroy()</code> 8 * method will be called every time the form shuts 9 * down for whatever reason. 10 * 11 * @see TagBehavior.Adapter 12 */ 13 14 public interface TagBehavior 15 { /** This method is called to get name=value pairs used 16 * as form data when the user hits the Submit button. 17 * @return a list of one or more newline-delimited 18 * name=value pairs or an empty string (<code>""</code>) 19 * if there is no data to add. These pairs are 20 * added to the form data. 21 */ 22 public String getFormData(); 23 24 /** This method is called when the user hits the Reset button. 25 * It should restore the object to its initial state. 26 */ 27 public void reset(); 28 29 /** This method is called when the user moves on to the next 30 * page or the window containing the form is shut down. 31 * Use this hook to release any global resources that 32 * the TagBehavior object might be using. 33 */ 34 public void destroy(); 35 36 /** A convenience class, implements {@link TagBehavior} with 37 * methods that do nothing. You can extend this class instead 38 * of implementing {@link TagBehavior} when you don't need 39 * to override all the methods of the interface. 40 */ 41 public static class Adapter 42 { public String getFormData(){ return ""; } 43 public void reset(); 44 public void destroy(); 45 } 46 } Listing 5. ContributingText.java 1 import javax.swing.*; 2 import java.awt.*; 3 4 import com.holub.ui.HTML.*; 5 6 class ContributingText extends JTextField 7 implements com.holub.ui.HTML.TagBehavior 8 { private final String value; 9 private final String name; 10 11 public ContributingText(String name, String initialValue) 12 { super(initialValue); 13 this.name =name; //Initialize from attributes specified in the tag. 14 this.value =initialValue; 15 } 16 17 public void reset() // Called when the Reset button is hit. 18 { setText(value); // Restores the initial value. 19 invalidate(); 20 } 21 22 public void destroy(){ /* nothing to do */ } 23 24 public String getFormData () // Add an attribute to return 25 { return name + "=" + getText(); // to the form processor, as if 26 }; // this component was an <input...>. 27 28 // Normal JTextField overrides to make the thing display at 29 // a reasonable size. 30 31 public Dimension getPreferredSize(){ return new Dimension(150,20);} 32 public Dimension getMinimumSize() { return getPreferredSize(); } 33 public Dimension getMaximumSize() { return getPreferredSize(); } 34 } Listing 6. InputActionHandler.java 1 package com.holub.ui.HTML; 2 3 import java.util.Properties; 4 import javax.swing.*; 5 import java.awt.*; 6 import java.awt.event.*; 7 8 9 /**...*/ 10 11 public class InputActionHandler implements TagHandler 12 { 13 private final HTMLPane pane; 14 InputActionHandler( HTMLPane pane ){ this.pane=pane; } 15 16 // This method is called when the <inputAction...> tag is 17 // first processed, when the HTML input is imported into 18 // the HTMLPane. 19 // 20 public JComponent handleTag(HTMLPane source, Properties attributes) 21 { 22 final String name = attributes.getProperty("name"); 23 final String value = attributes.getProperty("value"); 24 25 // Pressed must be declared final because it's accessed 26 // from an inner-class object. Implement it as a one-element 27 // array so that it can nonetheless be modified. 28 // 29 final boolean pressed[] = new boolean[]{ false } ; 30 31 // This class is the JComponent that will appear on the 32 // screen in place of the tag. 33 // 34 class ButtonTag extends JButton implements TagBehavior 35 { ButtonTag(String text){ super(text); } 36 public void reset() 37 public void destroy() 38 39 // Called by the HTMLPane when it assembles the 40 // Properties object that holds the form data 41 // in order to send that data to its listeners. 42 // 43 public String getFormData() 44 { 45 // If the button is pressed, the form data holds 46 // a name=value pair that holds the value "true" 47 // The name is specified in the tag. 48 49 return (name == null) 50 ? "" 51 : (name + "=" + pressed[0]) 52 ; 53 } 54 } 55 56 ButtonTag proxy = new ButtonTag(value); 57 proxy.setAlignmentY( HTMLPane.BASELINE_ALIGNMENT ); 58 59 // Set up to handle the button click. Set the "pressed" 60 // state true and call HTMLPane's handleInputActionTag(...) 61 // method, which gathers up the form data and sends it 62 // to the HTMLPane's listeners with an actionPerformed(...) 63 // message. The form data for the "proxy" is fetched 64 // from the ButtonTag object's getFormData(...) method, 65 // declared about 10 lines up. 66 // 67 proxy.addActionListener 68 ( new ActionListener() 69 { public void actionPerformed( ActionEvent e ) 70 { pressed[0]=true; 71 pane.handleInputActionTag(name); 72 } 73 } 74 ); 75 return proxy; 76 } 77 } LoggingThe HTMLPanel logs errors and warnings to the com.holub.ui logger. To view the messages from all programs that use HTMLPanel, modify the JAVA_HOME/jre/lib/logging.properties file, setting the .level and java.util.logging.ConsoleHandler.level properties to ALL. You can also put the following code into your program to turn on logging for that program only:import java.util.logging.*; //... static { Logger log = Logger.getLogger("com.holub.ui"); Handler h = new ConsoleHandler(); h. setLevel(Level.ALL); log.setLevel(Level.ALL); log.addHandler(h); } You can turn logging off by specifying Level.OFF instead of Level.ALL.Known problemsThis class is based on JEditorPane, which does not use the world’s best HTML parser. The following problems caused by Sun Microsystems’ parser aren’t fixed in the current implementation:The parser is dog slow.CSS support is seriously broken. Styles are useless.None of the new HTML 4 or XHTML tags are handled.The parser doesn’t handle tables well. Simple table nesting is okay, but complicated stuff fails miserably.<applet ...> tags are not supported—they are actually obsolescent as of HTML 4, but <object ...> tags are supported. Use the latter to embed your applets in a form. See the Java Plug-in docs for a discussion of the correct way to do this.The Java 1.4 parser does not handle <input type=submit name=x value=y> correctly. (According to Sun, this problem will be fixed in Java 1.5.) In particular, the name=value string is not placed in the form data as it should be, so you cannot use multiple type=submit input fields in a single form. The new <button> and <input type=button> elements aren’t supported either, so you can’t use that tag.The JEditorPane base class doesn’t understand script tags (or JavaScript); it just displays as normal text the contents of all script elements not nested inside comments. Unfortunately, javadoc-generated HTML doesn’t follow the nest-script-in-comments convention, so you can’t easily use JEditorPane as a Java-documentation browser.The JEditorPane doesn’t do frames correctly when you customize it (as I’ve done here). Frames appear to work, but pages displayed within frames are processed as if you were using a standard javax.swing.JEditorPane, with none of the features described here available to you. Consequently, you can’t really use HTML frames. You can, however, create several HTMLPane instances and arrange them inside a JPanel (or other container) using a GridBagLayout or GridLayout.I hope that at least some EditorKit behavior will eventually get fixed. My guess is that the more fundamental structural problems (like the broken frame stuff) will probably stay broken, so the only long-term solution is to toss the Sun implementation and replace it with something that works.Design notesFinally, some of you seemed to miss my comments last month about how difficult it is to get rid of accessor and mutator methods at the procedural boundary of a system and in generic classes, so let me address the I-told-you-so comments that will otherwise appear in the Talkback discussion.The client-side UI-builder classes are both on the procedural boundary and generic. As such, you can’t eliminate accessors and mutators altogether, though you can minimize their number.Even though the HTMLPane does, by necessity, expose a Properties object that contains the form data, the implementation of the HTMLPane is still hidden (at least as much as it can be, considering that it’s based on JEditorPane). The exposed form data flows through the HTMLPane. It’s provided as input and shows up in the output. The HTMLPane is a caretaker of the data, but the nature of that data is not central to the behavior of the class.There’s a good analog in Java’s collection classes. You can put objects into the collection and take them out again, but you have no idea how the collection actually works. The collection’s implementation is hidden. The collection is simply a caretaker of data you provide, data it knows absolutely nothing about. The design pattern, here, is Memento.Don’t confuse the classic procedural-design notion of data hiding with the purely OO notion of implementation hiding. (Last month’s column dealt with the latter.) The acid test is: can you change the implementation without affecting the client classes? The code I added to JEditorPane in HTMLPane passes that test.My design decision to use an actionListener and an Event derivative is also a good example of the sorts of compromises you have to make in the real world. The ActionListener mechanism is a natural one to use here, given the way this mechanism is used in other Swing components. Though I could have introduced a different sort of Observer object that was passed the synthesized property list directly (thereby eliminating the need for a data() accessor, which seemed like a bad idea in the current context. I wanted a Swing programmer to be able to use the HTMLPane intuitively. As I said last month, design is a series of compromises.In closingI’ve used HTML layout to good effect in several programs, and it certainly does make life easier. The significant downside of the HTML approach is that it’s difficult to create a good user experience with a HTML-based user interface, whether it’s created on the client side or server side. There are classes of programs, however, for which HTML is just fine, and using HTML on the client side can significantly reduce the amount of time it takes to build the UI. As an added benefit, it’s possible to create a UI that can move between client and server side with minimal difficulty.Next month I’ll explain how HTMLPane is implemented and look at a few more of its methods.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