Java applets can bring dynamic functionality to your wiki pages Wikipedia may be the most high-profile example of a user-edited wiki site, but millions of people around the world are setting up wikis for knowledge-sharing on a smaller scale. In this article, Randall Scarberry demonstrates an interesting way to add dynamic functionality to a wiki — using good old-fashioned Java applets.Virtually everyone familiar with today’s World Wide Web has encountered the free online encyclopedia, Wikipedia. Wikipedia is driven by an excellent open source product called MediaWiki, which is also available for free. This has led to a proliferation of wiki sites devoted to just about any topic you can imagine. Users of a wiki can add content — all they need to do is type their additions into their Web browsers using the simple markup language called wikitext. Even better, the developers of wikitext made it extensible. With a little server-side development of your own, you can add your own custom syntax. Users aware of your extensions can then utilize them on their wiki pages with a few simple keystrokes. These extensions can include custom decorations, formatting, Web applications, and even instances of the venerable Java applet. Jmol, for instance, can be used to embed a 3D molecular viewer into a wiki page.In this article, you’ll walk through the steps of deploying a fairly elaborate applet as a MediaWiki extension. Though the example is by no means exhaustive — an entire book would be required to explain everything about the subject — it does demonstrate how to give the applet resize handles using a little JavaScript, CSS coding, and some popular JavaScript libraries. You’ll also see how to customize the extension using a wiki template. Finally, you’ll get a look at a rudimentary persistence mechanism that allows applets to save data directly to the wiki pages on which they reside. The example applet and wiki setupThis article employs an example applet, shown in Figure 1, that compares K-Means and X-Means clustering on small amounts of generated data.Figure 1. Demonstration applet deployed as a MediaWiki extensionK-Means? X-Means? What does that mean?If you know nothing about the mathematical discipline of clustering analysis, these terms probably don’t mean a whole lot to you. Fortunately, you don’t really need to know the details to follow along with the example. A quick primer: given some number of points in N-dimensional space (this example uses two-dimensional space to keep things simple), clustering groups the points by Euclidean distance. In other words, clustering attempts to identify the clumps. K-Means groups them into exactly the number of requested clusters (K), while X-Means attempts to determine how many clumps are in the distribution using a statistical measure. Before you can use the demo application (which I’ll refer to as clustering_demo for the remainder of this article), you need to have developer and administrator access to your own MediaWiki site. To set up your own wiki, you need some free software, a moderately capable computer, and an Internet connection. If you do not yet have a wiki set up, go to the MediaWiki homepage, download MediaWiki, and follow the installation instructions on the site. During the procedure, you will need to also download and install PHP and an open source database such as MySQL. And, of course, you need to have a Web server, such as Apache.Assuming that you have installed MediaWiki and initialized your wiki site, you can hook in clustering_demo using the following procedure: Download and save the zip file accompanying this article, clustering_demo.zip, onto the computer on which your MediaWiki server resides.Open the file you downloaded in a zip file browser (or expand it to a temporary directory and navigate to that directory with a file browser).In another file browser window, find the MediaWiki installation directory, which should have a name like mediawiki-1.12.0 (the name you see will depends upon the version of MediaWiki you installed). This directory will also contain a file named LocalSettings.php.If the MediaWiki installation directory does not already contain a subdirectory named “extensions,” create it.In the zip file browser, change to the deployment directory. Extract or copy the subdirectory named “clustering_demo” to the extensions directory you found or created in Step 4. Be sure to copy the directory’s contents, including its subdirectories, and keep the relative file paths.Open LocalSettings.php in a text editor. At the bottom of the file, add the line in Listing 1.Listing 1. Adding clustering_demo to LocalSettings.phprequire_once("extensions/clustering_demo/clustering_demo.php"); Then save the file.To disable caching while you are working through this example, add the lines in Listing 2 to LocalSettings.php.Listing 2. Disabling caching$wgMainCacheType = CACHE_NONE; $wgMessageCacheType = CACHE_NONE; $wgParserCacheType = CACHE_NONE; $wgCachePages = false; If caching is not disabled, you may have to do more than simply reload Web pages in your browser before you can see the effects of the changes to the PHP code. Having to restart servers to see the effects of minor edits can be quite frustrating.Put on your wiki user hat and create a new test page on your wiki. Point your Web browser to a page on your wiki server that does not yet exist. For example, if you’re working on the server machine itself, direct the browser to http://localhost/mediawiki/index.php/ClusteringDemoTest.Click on the new page’s Edit tab. In the text area of the form that appears, type <clustering />, and then click the Save Page button.Assuming that all went well, the applet from Figure 1 should now be sitting proudly in your browser. The four text fields in the upper-left corner allow you to type in the number of 2-D test coordinates, the number of clusters, a random number seed to be used throughout, and the standard deviation of the cluster distributions. If all fields have values, the button labeled Generate Test Data and Cluster kicks off the clustering processes. First the data is generated, then K-Means is run, and then X-Means. The text area displays status messages as the algorithms move along. On the right are three scatter plots implemented using the excellent open-source library JfreeChart 1.0.4. The top plot shows the data as generated, the middle shows the K-Means clusters, and the bottom shows the results from X-Means. The Save Settings button copies the test settings to a row in the table at the lower left, associating them with a comment. Clicking on the Restore Settings button copies the settings from the selected table row back into the text fields, so you can rerun them and relive the glories of clustering past.Step 6 of the procedure hooks the extensions into MediaWiki. These extensions, defined in clustering_demo.php, take two different forms: tag extensions and parser functions. The chief difference between them is the form of the wikitext you type into your Web browser to embed the applet extension on the page of the wiki. Tag extensions use an XML syntax. Step 9 above employs the tag extension format, with the tag name clustering. The parser function format uses a syntax that is somewhat more cryptic, but shorter when you need to supply parameters to the extension. If Step 9 had used the parser function format, you would have entered {{#clustering:}} to embed the applet. Embed the applet using a tag extensionThe extension module file that you installed in Step 6 is the deluxe version, containing all the bells and whistles. Over the course of this article, you’ll learn how to build this file, going from simple to complex, so that you gain an understanding of what each part does. I’ll begin by describing how to embed the applet using a tag extension. Under the MediaWiki installation directory, open extensions/clustering_demo/clustering_demo.php in a text editor and replace its contents with the code shown in Listing 3. (This version of the file is in the listings/listing_3 directory of the zip file, if you would rather just copy it over. The listings in the zip are liberally commented.) Listing 3. Implementing the tag extension<?php $wgExtensionFunctions[] = 'clusteringDemoSetup'; function clusteringDemoSetup() { global $wgParser; $wgParser->setHook('clustering', 'clusteringDemoRender'); } function clusteringDemoRender($input, $args, $parser) { $output = "<applet name='clustering_demo_applet' " . "id='clustering_demo_applet' code='clustering.DemoApplet' " . "archive='clustering_demo.jar' " . "codebase='/mediawiki/extensions/clustering_demo' width='700' height='700'>n"; foreach ($args as $name => $value) { $output .= " <param name='" . htmlspecialchars($name) . "' VALUE='" . htmlspecialchars($value) . "'>n"; } $output .= "</applet>"; return $output; } ?> The PHP code in this article is not complex, but some items may be new to Java developers. So, here are a few things to note about PHP:PHP files begin with <?php and end with ?>.Variables always begin with a dollar sign.Within a PHP function, global variables are not accessible unless you declare them within the function preceded by the word global.PHP arrays are more similar to Java maps than to Java arrays, and their sizes are dynamic. Also, because PHP is loosely typed, a PHP array may contain multiple data types.To add an element to a linear PHP array, you’d use a syntax that may appear unusual to a Java developer. For example, $myArray[] = "another element"; adds the string “another element” to the array $myArray.The string concatenation operator in PHP is the period, not the plus sign.PHP strings may be surrounded by either double or single quotes. As in shell programming, variable substitution takes place in double-quoted strings but not in single-quoted strings.MediaWiki’s global variables are usually prefixed by $wg. In Listing 3, the variables $wgExtensionFunctions and $wgParser are both MediaWiki globals.When your Web browser requests a wiki page, MediaWiki fetches the unrendered page text from the back-end database, then runs the text through a rendering engine or parser. The parser calls your extension-rendering functions when it encounters registered tags.Back in Listing 3, the first line of code adds the name of the function clusteringDemoSetup to the global array $wgExtensionFunctions so that MediaWiki executes the function automatically every time a page is requested. The function clusteringDemoSetup calls the global MediaWiki parser’s method setHook to declare the word clustering as a tag extension associated with the rendering function clusteringDemoRender. Words defined as tag extensions are used by embedding them in the page text as XML. Thereafter, whenever MediaWiki encounters <clustering /> or <clustering ...>...</clustering> in the unrendered text, it calls clusteringDemoRender to transform it into HTML. The clusteringDemoRender function churns out the standard HTML used for embedding a Java applet. The argument $input is the text between the opening and closing XML tags <clustering> and </clustering> and is not used by this example. However, the second function argument $args is very important. It is an associative array (like a java.util.Map) whose keys are the names of the XML tag attributes and whose values are the attribute values. The function’s foreach loop simply passes them onto the applet as parameters. To see how this works, go back to the edit tab of your test page and change <clustering/> to the code in Listing 4.Listing 4. Changing the applet parameters<clustering coordinates="2500" clusters="15" randomSeed="6789" standardDev="0.02"> This part is ignored by the example. </clustering> You will see in the rendered page that the four parameters initialize the text fields of the applet. They are all optional, and the applet assumes default values if no other values are provided.When you edited the text, you probably saw a block of XML of the format <clustering_data>...</clustering_data>. This is data that was inserted by the applet when you clicked the Save Settings button. Do not let this confuse you right now. You may delete this block, but any saved settings in the applet table will vanish if you do so. The last section of the article will explain this in more detail. Dress it up: Adding resize handles and stylingJust about every applet you encounter while browsing the Web has fixed dimensions. But in this era of dynamic Web pages with Cascading Style Sheets and DOM scripting, the size of an applet no longer has to be static. I’ll buck convention by describing how to add resizing handles to the applet using a combination of JavaScript and CSS.Notice that the clustering_demo extension directory contains two subdirectories, css and js. These are for Cascading Style Sheets and JavaScript files, respectively. In css, there is a single stylesheet named clustering_demo.css. The js directory contains the file clustering_demo.js, which implements resize handles for the applet using the open-source Prototype 1.6.0.2 and script.aculo.us 1.8.1 libraries; the files for these libraries are located in the lib subdirectory of js.But these files never make it to your browser in the current simple version of cluster_demo.php. So, open the file and edit the function clusteringDemoSetup to match the version shown in Listing 5. (The complete file is located in listings/listing_5 of the zip file.) Listing 5. Adding resize handles to the appletfunction clusteringDemoRender($input, $args, $parser) { global $wgOut, $wgScriptPath; $wgOut->addScript('<link rel="stylesheet" type="text/css" href="' . $wgScriptPath . '/extensions/clustering_demo/css/clustering_demo.css" />'); $wgOut->addScript('<script src="' . $wgScriptPath . '/extensions/clustering_demo/js/lib/prototype.js"' . ' type="text/javascript"></script>'); $wgOut->addScript('<script src="' . $wgScriptPath . '/extensions/clustering_demo/js/lib/scriptaculous.js' . '?load=effects,dragdrop" type="text/javascript"></script>'); $wgOut->addScript('<script src="' . $wgScriptPath . '/extensions/clustering_demo/js/clustering_demo.js"' . ' type="text/javascript"></script>'); $output = "<div id='clustering_demo_div'>" . "<applet name='clustering_demo_applet' id='clustering_demo_applet'" . " code='clustering.DemoApplet' archive='clustering_demo.jar' " . "codebase='/mediawiki/extensions/clustering_demo'>n"; foreach ($args as $name => $value) { $output .= " <param name='" . htmlspecialchars($name) . "' VALUE='" . htmlspecialchars($value) . "'>n"; } $output .= "</applet>" . "<div id='clustering_demo_right_handle'></div>" . "<div id='clustering_demo_corner_handle'></div>" . "<div id='clustering_demo_bottom_handle'></div>" . "</div>n"; return $output; } You will notice that clusteringDemoRender has several new lines that call the addScript function of the global MediaWiki object $wgOut. These calls simply inject their string arguments into the head section of the Web page. The first adds a link to the stylesheet and the others insert references to the JavaScript files. The JavaScript in clustering_demo.js sets up resize handles around the applet with its function initResizeHandles, which is called automatically when the page finishes loading. Reload the page after saving the new version of clustering_demo.php and try them out.To make the handles work, the applet is enclosed in a parent div element with three div siblings for the handles. These all have self-explanatory names that are referenced by the stylesheet as well as by clustering_demo.js. This article is too short to explain the workings of the CSS and JavaScript files; however, a careful reading of the comments should get you well on your way to figuring out how they work. As noted, the JavaScript uses the popular Prototype library, whose style is heavily influenced by Ruby. You may find it quite disconcerting at first, but after a little study you will find its syntax to be elegant, simple, and well worth the effort of learning.Another syntax: Defining a parser functionAs stated previously, the sample application could also use another wikitext format, called the parser function. To implement the extension as a parser function, go back to clustering_demo.php and enter the new code shown in Listing 6. (To shorten the listing, the parts of the file unchanged from Listings 3 and 5 are omitted. The entire file is contained in the zip file directory listings/listing_6.) Listing 6. Adding the parser function format$wgExtensionFunctions[] = 'clusteringDemoFunctionSetup'; $wgHooks['LanguageGetMagic'][] = 'clusteringDemoFunctionMagic'; function clusteringDemoFunctionSetup() { global $wgParser; $wgParser->setFunctionHook('clustering', 'clusteringDemoFunctionRender'); } function clusteringDemoFunctionMagic(&$magicWords, $langCode) { $magicWords['clustering'] = array(0, 'clustering'); return true; } function clusteringDemoFunctionRender(&$parser, $coordinates = '', $clusters = '', $randomSeed = '', $standardDev = '') { $output = "<clustering"; if ($coordinates != '') { $output .= " coordinates = "$coordinates""; } if ($clusters != '') { $output .= " clusters = "$clusters""; } if ($standardDev != '') { $output .= " standardDev = "$standardDev""; } if ($randomSeed != '') { $output .= " randomSeed = "$randomSeed""; } $output .= " />"; return $parser->recursiveTagParse($output); } After saving the new version of clustering_demo.php, embed the applet in the page using the format shown in Listing 7.Listing 7. Embedding the applet in parser function format{{#clustering: [coordinates] | [clusters] | [randomSeed] | [standardDev] }} The parameters, separated by pipe symbols, are all optional. However, when provided, they must be placed in the proper order. The number of coordinates must be in the first position, the number of clusters must be in the second, and so on. Some examples using this format are shown in Listing 8.Listing 8. Examples of the parser function format{{#clustering: 2500 | 30 | 3456 | 0.025 }} {{#clustering: 3000 }} {{#clustering: | 24 | 6789 }} {{#clustering: 1400 | | 8989 }} The first example in Listing 8 supplies all four parameters, while the second supplies only the number of coordinates and lets the other parameters assume default values. The third supplies the number of clusters and the random seed, but not the number of coordinates and standard deviation. The fourth supplies the number of coordinates and the random seed, but lets the other two parameters assume default values. Listing 6 triggers the setup of the parser function extension by adding the name of the clusteringDemoFunctionSetup function to $wgExtensionFunctions. Upon execution, this function calls the $wgParser method setFunctionHook (not setHook as in clusterDemoRender) to associate the rendering function clusteringDemoFunctionRender with a new parser function named clustering.So far, this is very similar to the way the tag extension was set up. But there is a little more work to do. A list of so-called magic words must be associated with the parser function. This is accomplished by the function clusteringDemoFunctionMagic, whose name is added to the array contained in $wgHooks['LanguageGetMagic']. As with the functions whose names are added to $wgExtensionFunctions, the functions in $wgHooks['LanguageGetMagic'] are run by MediaWiki at startup. The function clusteringDemoFunctionMagic associates a list of magic words with “clustering”. It does this on the line that creates an array using PHP’s array function. The first element of the array must be 0 or 1 and the remainder of the elements must be the magic words. The entry 0 for the first element indicates that the words are to be case insensitive. The magic words are what you must actually use on the edit tab to embed the extension. Typically, you only define one magic word, making it the same as the name of the parser function defined in the call to setFunctionHook. However, if the line in clusteringDemoFunctionMagic had been what you see in Listing 9, you could have also embedded the applet by entering either {{#foo:}} or {{#bar:}}.Listing 9. Using more than one magic word$magicWords['clustering'] = array(0, 'clustering', 'foo', 'bar'); The rendering function clusteringDemoFunctionRender receives a reference to the parser and each of the function parameters. Rather than generating HTML directly, it generates output in the tag extension format, which it hands off to the parser method recursiveTagParse. This method forwards the output to the function clusteringDemoRender, which remains the same as it was in the previous listing. The ultimate rendering logic therefore remains in a single place. User customization: Defining a template for the extensionOne reason the parser function format is handy is that it can be employed by a user-defined MediaWiki template. Templates allow users to create standard formats for wiki pages from their Web browsers. For example, a user may want to precede the applet with the same heading each time it is embedded, or always follow it with a descriptive paragraph. The template concept is easily explained by an example, so put on your user hat again and create a template called ClusteringTemplate. Open up the edit tab of your test page and replace the text used to embed the extension with the line in Listing 10.Listing 11. Defining the clustering demo template==K-Means vs. X-Means Comparison== {{#clustering:}} The applet above compares simple implementations of K-Means and X-Means clustering on small sets of two-dimensional test vectors. Enter the number of vectors (points) for the test data, the number of clusters, the random seed, and the standard deviation. The random seed is specified for repeatability. The standard deviation affects the tightness of the clusters. Save the page, then go back and view your test page. You will see the applet once again, but it will be preceded by a heading and followed by the paragraph of text you entered in Listing 11. (Note that in wikitext, second-level headings are surrounded by two equal signs.)That’s all well and good, but what if you want to pass values for one or more of the four applet parameters? Go back to the edit tab of Template:ClusteringTemplate and change the line in double curly braces to the line in Listing 12. Listing 12. Adding parameters to the template{{#clustering: {{{coordinates|2000}}} | {{{clusters|20}}} | {{{randomSeed|1234}}} | {{{standardDev|0.02}}} }} Template parameters are specified by name and surrounded by groups of three braces. Within each group of curly braces, the name of the parameter is followed by a pipe character and the parameter’s default value. If you want the default value to be blank, type nothing after the pipe symbol.Save the updated template, then try passing parameter values from your test page with entries like the one in Listing 13.Listing 13. Passing parameter values to a wiki page{{clusteringTemplate|coordinates=2500|clusters=15|randomSeed=2222|standardDev=0.025}} The parameters to templates are named and are separated from other parameters and from the template name by pipe characters. They are all optional, and position does not matter. You could pass clusters before coordinates with no trouble. If you need to pass parameters, your template must use the parser function format instead of the tag extension format. This would not work in the template definition shown in Listing 14.Listing 14. Don’t pass parameters like this<clustering coordinates="{{{coordinates|1000}}}" /> This fails because the parser passes control to the tag extension rendering function before substituting parameters. But with parser functions, the parameters are substituted before the parser function renderer executes.Simple persistence: Saving applet data to the wiki pageI’ll conclude this discussion by showing you a simple approach to persisting applet data directly to the wiki page on which it resides. For more complex applets, it would be preferable to design a server module that manages the applet data in its own specialized database; but for simple applets such as this demo, the following approach suffices, and as an added bonus demonstrates how an applet can successfully post edits to the wiki. Before getting into the applet communication code, look over the final additions to clustering_demo.php, shown in Listing 15.Listing 15. Passing communication parameters to the appletfunction clusteringDemoRender($input, $args, $parser) { global $wgOut, $wgScriptPath, $wgCookiePrefix, $clusteringDemoAppletEmbedded; if ($clusteringDemoAppletEmbedded) { return "Only one clustering applet per page!"; } $clusteringDemoAppletEmbedded = true; $wgOut->addScript('<link rel="stylesheet" type="text/css" href="' . $wgScriptPath . '/extensions/clustering_demo/css/clustering_demo.css" />'); $wgOut->addScript('<script src="' . $wgScriptPath . '/extensions/clustering_demo/js/lib/prototype.js"' . ' type="text/javascript"></script>'); $wgOut->addScript('<script src="' . $wgScriptPath . '/extensions/clustering_demo/js/lib/scriptaculous.js' . '?load=effects,dragdrop" type="text/javascript"></script>'); $wgOut->addScript('<script src="' . $wgScriptPath . '/extensions/clustering_demo/js/clustering_demo.js"' . ' type="text/javascript"></script>'); $output = "<div id='clustering_demo_div'>" . "<applet name='clustering_demo_applet' id='clustering_demo_applet'" . " code='clustering.DemoApplet' archive='clustering_demo.jar' " . "codebase='/mediawiki/extensions/clustering_demo'>n"; $gotExtensionTag = false; $gotExtensionType = false; foreach ($args as $name => $value) { if (!$gotExtensionTag && $name == 'extensiontag') { $gotExtensionTag = true; } if (!$gotExtensionType && $name == 'extensiontype') { $gotExtensionType = true; } $output .= " <param name='" . htmlspecialchars($name) . "' VALUE='" . htmlspecialchars($value) . "'>n"; } $cookieNames = array("UserName", "UserID", "_session"); foreach($cookieNames as $ndx => $value) { $cookieKey = $wgCookiePrefix . $value; if (array_key_exists($cookieKey, $_COOKIE)) { $output .= " <param name='" . htmlspecialchars($value) . "' VALUE='" . htmlspecialchars($_COOKIE[$cookieKey]) . "'>n"; } } $output .= " <param name='cookiePrefix' VALUE='" . htmlspecialchars($wgCookiePrefix) . "'>n"; if (!$gotExtensionTag) { $output .= " <param name='extensiontag' VALUE='clustering'>n"; } if (!$gotExtensionType) { $output .= " <param name='extensiontype' VALUE='tag'>n"; } $output .= " <param name='dataTag' VALUE='clustering_data'>n"; $output .= "</applet>" . "<div id='clustering_demo_right_handle'></div>" . "<div id='clustering_demo_corner_handle'></div>" . "<div id='clustering_demo_bottom_handle'></div>" . "</div>n"; return $output; } The main difference in this new version of the function is that it now passes a number of new parameters that the applet requires for successful communication with the server. The most important ones are the values of three cookies containing the user name, ID, and session, which are passed to the applet as the parameters UserName, UserID, and _session, respectively. Whenever the applet attempts to save data to the server, it sends these items to the server as cookies in the header of the HTTP request. Because a MediaWiki server may be configured to allow edits only for users who are logged in, it is imperative to send back the session identifier with the edits.Also important is the cookie prefix, defined in the MediaWiki global $wgCookiePrefix. Because every MediaWiki server can define its own cookie prefix, the applet must be told what prefix to use so that it can post the cookies to the server using names that the server understands. The cookie names are simply the cookie prefix concatenated with the names UserName, UserID, and _session. The prefix is passed to the applet as the parameter cookiePrefix.As I will explain later, the applet also needs to know the method used to embed it in the Web page. This information is passed to the applet in the parameters extensionTag and extensionType. For this example, extensionTag is either clustering or the name of the template, but extensionType can be either tag, function, or template. The final parameter that the function passes to the applet is dataTag, which tells the applet what XML tag to use for storing its data. The example uses clustering_data. Another thing you may have noticed in Listing 15 is that clusteringDemoRender uses the flag variable $clusteringDemoAppletEmbedded to permit only one demo applet on the page. This limitation is necessary because the Java code that saves the data always associates the data with the first applet it finds on the page. Rather than complicating the code to make it work with multiple applets, I added this restriction, because you’re not likely to want multiple applets on a single page.You can’t tell from the listing, but clusteringDemoFunctionRender has two additional arguments, named $extensionTag and $extensionType; these default to clustering and function, respectively. For persistence to work with the template, you must have the template use clusteringTemplate and template for these two parameters. So return to the edit tab of the page Template:ClusteringTemplate and change the parser function part of the content to what you see in Listing 16.Listing 16. Making persistence work with the template{{#clustering: {{{coordinates|2000}}} | {{{clusters|20}}} | {{{randomSeed|1234}}} | {{{standardDev|0.02}}} | clusteringTemplate | template }} Before you read further, replace clustering_demo.php with the final version, found in the zip file in listings/listing_15. (Do not simply modify the file with the code from Listing 15, as that listing does not show all the changes.) Now reload your test page and try out the applet’s Save Settings button a few times. This button doesn’t just add new rows to the table in the lower left of the applet; it also saves the data to the wiki page. You can confirm this by looking at the edit tab for the page. Just after the extension text, you will see the data encapsulated within the XML tags <clustering_data> and </clustering_data>.Server communicationTo see how the server communication works, take a look at some select listings from the key communication class, clustering.MediaWikiComm. This class uses the open source Jakarta Commons HttpClient 3.1 library for both posting data to and retrieving it from the server. Two other classes whose listings will not be shown, but whose thoroughly commented code can be found in the zip file, are clustering.ClusteringDemoSettings and clustering.ClusteringDemoSettingsFactory. ClusteringDemoSettings is a simple immutable class whose instances represent the rows of settings shown in the table. ClusteringDemoSettingsFactory contains static methods to render XML from the array of ClusteringDemoSettings objects underlying the table. It also parses XML from the page and transforms it into ClusteringDemoSettings objects.The main panel (the clustering.ClusteringDemoPanel class) instantiates a single instance of MediaWikiComm by calling the constructor with the signature in Listing 17.Listing 17. Signature for the MediaWikiComm constructorpublic MediaWikiComm(String pageURL, String extensionTag, String dataTag, String extensionType, String[] cookiePairs); The pageURL always has the format http://<servername>/mediawiki/index.php?title=<page title>. The arguments extensionTag, dataTag, and extensionType are supplied from the applet parameters having those names. The array cookiePairs contains name-value pairs to be sent in the cookie header entries of server requests; the even elements are the names of the cookies and the odd elements are the values. These are constructed from the cookie-related applet parameters by the parseCookiePairs() method on the applet class clustering.DemoApplet.The first duty of the MediaWikiComm object is to download saved settings for populating the ClusteringDemoPanel table immediately after the applet loads. The code that does this is shown in Listing 18.Listing 18. Loading saved settings from the wiki pagepublic ClusteringDemoSettings[] loadData() throws IOException { mPageData = loadEditFormData(); return ClusteringDemoSettingsFactory.parseFromContent( mPageData.getEditText(), mDataTag); } public EditFormData loadEditFormData() throws IOException { GetMethod method = new GetMethod(mPageURL + "&action=edit"); int responseCode = 200; String responseBody = null; method.getParams().setCookiePolicy(CookiePolicy.IGNORE_COOKIES); String cookies = getCookieString(); if (cookies.length() > 0) { method.setRequestHeader("Cookie", cookies); } try { responseCode = mHttpClient.executeMethod(method); if (responseCode >= 200 && responseCode < 300) { String charSet = method.getResponseCharSet(); BufferedReader br = new BufferedReader( new InputStreamReader(method.getResponseBodyAsStream(), charSet)); StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw); String line = null; while((line = br.readLine()) != null) { pw.println(line); } pw.flush(); responseBody = sw.toString(); } else { // Bad response code -- return an IOException with the status. throw new IOException(method.getStatusText() + " (" + responseCode + ")"); } } finally { method.releaseConnection(); } HtmlForm form = HtmlUtilities.extractForm("editform", responseBody); String empty = ""; String startTime = empty, editTime = empty, editText = empty, autoSummary = empty, editToken = empty; if (form != null) { int sz = form.getElementCount(); for (int i=0; i<sz; i++) { HtmlForm.Element element = form.getElement(i); HtmlForm.ElementAttribute nameAttr = element.getElementAttributeByName("name"); HtmlForm.ElementAttribute valueAttr = element.getElementAttributeByName("value"); String name = nameAttr != null ? nameAttr.getValue() : null; try { if (WP_STARTTIME.equals(name)) { startTime = valueAttr.getValue(); } else if (WP_EDITTIME.equals(name)) { editTime = valueAttr.getValue(); } else if (WP_TEXTBOX1.equals(name)) { BufferedReader br2 = new BufferedReader( new StringReader(element.getEmbeddedText())); StringWriter sw2 = new StringWriter(); PrintWriter pw2 = new PrintWriter(sw2); String line2 = null; while((line2 = br2.readLine()) != null) { pw2.println(line2); } pw2.close(); try { br2.close(); } catch (IOException wontHappen) {} editText = sw2.getBuffer().toString(); } else if (WP_AUTOSUMMARY.equals(name)) { autoSummary = valueAttr.getValue(); } else if (WP_EDITTOKEN.equals(name)) { editToken = valueAttr.getValue(); } } catch (RuntimeException rte) { rte.printStackTrace(); throw new IOException("invalid editform data: " + rte.toString()); } } // for (int i... } else { // form == null throw new IOException("no edit form on page"); } return new EditFormData(startTime, editTime, editText, autoSummary, editToken); } The loadData() and loadEditFormData() methods download the data for the wiki page in edit mode — that is, they download the unrendered source behind the edit tab for the page. To understand this, go to the edit tab in your browser, then view the page source. What you see is the HTTP GET request that the second method downloads. Among all the complex HTML and JavaScript, there is a form element with the ID editform. The page content is found in the textarea element of that form with the name and ID wpTextbox1.The loadData() method returns an array of ClusteringDemoSettings objects. It gets this information from the page first by calling loadEditFormData() to retrieve the edit form information as an instance of the nested static class EditFormData. It then calls the parseFormContent() method on ClusteringDemoSettingsFactory to extract the array of settings objects from the content of wpTextbox1 (accessed via mPageData.getEditText()).The loadEditFormData() method downloads the HTML source for the edit tab using a simple HTTP GET with the URL-encoded parameter string action=edit. After executing the request, the method reads the response line-by-line and places it in the variable responseBody. A call to HtmlUtilities.extractForm() then parses the form editform into an instance of the utility class HtmlForm. The HtmlForm class provides methods for stepping through the attributes and values of the form’s elements. The lengthy for-loop that follows extracts the values for the elements with the names wpStarttime, wpEdittime, wpTextbox1, wpAutoSummary, and wpEditToken. As discussed previously, wpTextbox1 contains the page content in its embedded text.The remaining parameters are very important to ensure that edits post successfully. Once these items are extracted, the method returns them in an instance of EditFormData, which is kept in the MediaWikiComm object variable mPageData.The MediaWikiComm object saves changes back to the page by pretending to be a browser posting changes entered on the edit tab. Just as a browser does, it posts the edit form elements as multipart form data. Listing 19 shows the code that does this, all in the saveData() method.Listing 19. Posting edits to the MediaWiki serverpublic void saveData(ClusteringDemoSettings[] settings) throws IOException { String newEditText = ClusteringDemoSettingsFactory.updateContent( mPageData.getEditText(), mExtensionTag, mDataTag, mExtensionType, settings); if (newEditText == null) { throw new IOException("could not generate updated page content"); } PostMethod method = new PostMethod(mPageURL + "&action=submit"); int responseCode = 200; method.getParams().setCookiePolicy(CookiePolicy.IGNORE_COOKIES); String cookies = getCookieString(); if (cookies.length() > 0) { method.setRequestHeader("Cookie", cookies); } Part[] parts = { new StringPart(WP_SECTION, ""), new StringPart(WP_STARTTIME, mPageData.getStartTime()), new StringPart(WP_EDITTIME, mPageData.getEditTime()), new StringPart(WP_SCROLLTOP, ""), new StringPart(WP_TEXTBOX1, newEditText), new StringPart(WP_SUMMARY, ""), new StringPart(WP_SAVE, "Save page"), new StringPart(WP_EDITTOKEN, mPageData.getEditToken()), new StringPart(WP_AUTOSUMMARY, mPageData.getAutoSummary()) }; method.setRequestEntity(new MultipartRequestEntity(parts, method.getParams())); try { responseCode = mHttpClient.executeMethod(method); if (responseCode >= 200 && responseCode < 300) { throw new ConcurrentEditException(); } else if (responseCode != 302) { throw new IOException(method.getStatusText() + " (" + responseCode + ")"); } } finally { method.releaseConnection(); } } Before posting the changes to the server, saveData() calls ClusteringDemoSettingsFactory‘s updateContent() method to generate a new version of the edit text. This method scans the current version of the edit text for a block of XML with a root element with the value of mDataTag. In other words, because mDataTag equals clustering_data, it scans for <clustering_data>(old settings XML)</clustering_data>.If the method finds what it’s looking for, it replaces the old settings between the XML tags with the new version generated from the array of ClusteringDemoSettings objects currently in the table. However, if there is no block of old settings data — when data is saved from the applet for the first time, for instance — updateContent() finds no block of old settings XML to replace. Therefore, it has to search for the text used to embed the applet, so it can place the block of settings data immediately after. Because the applet may be embedded using a tag extension, a parser function, or template syntax, the method needs to perform a search using regular expressions appropriate to the extension method. The commenting in the source for ClusteringDemoSettingsFactory explains this in more detail.After the call to updateContent(), saveData() makes a multipart form data HTTP POST to the server using the HttpClient library. The editform elements that were stored in the EditFormData object mPageData are posted back to the server. The value for wpEditToken is of particular importance, as the MediaWiki server matches it with the user’s session identifier. (Recall that the user name, ID, and session are posted in the cookie header of the request.)You might assume that successfully posting an edit would return a response code of 200, but this is not the case. Instead, the server sends a response code of 302 and attempts to redirect the browser to the rendered version of the page. When you edit from the browser, this is exactly the behavior you expect. The browser does not remain on the edit tab: it automatically redirects to the new, rendered version of the Web page. When the server returns 200 (which is what you would normally think of as the success code), it typically means that the edit failed because another user posted an edit since you last downloaded the page data. When this happens, saveData() throws a MediaWikiComm.ConcurrentEditException. The applet responds to such an exception by reloading the data from the page, merging it with the new data, and attempting the save again.You may be wondering why the XML block for the applet data has no effect on the appearance of the rendered Web page, other than the contents of the applet’s table. If you look at the complete listing for clustering_demo.php, you will see that the function clusteringDemoSetup contains an additional call to setHook that associates the tag clustering_data with the function blankRender. As its name suggests, this function returns a blank string. If this null renderer had not been associated with clustering_data, the XML code would have been unattractively visible beneath the applet.In conclusionIn this article, you’ve seen how to begin deploying Java applet extensions on MediaWiki-based sites. If you would like to learn more about developing your own MediaWiki extensions, try perusing the contents of the accompanying zip file thoroughly. I also encourage you to visit the MediaWiki site and read its section about extension development.Randall Scarberry is a senior research scientist and software engineer at the Department of Energy’s Pacific Northwest National Laboratory in Richland, Washington. He specializes in high-performance Java applications for pattern recognition and text analysis. When not coming up with ways to speed up tricky algorithms, he enjoys hiking the numerous trails in the Cascade Mountains. Open SourceSoftware DevelopmentWeb DevelopmentJavaPHP