Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

JVM Languages

Incrementally Updating Software


John is a long time consultant and the author of numerous shareware and freeware programs. He can be contacted at [email protected].


Although there has been a lot of work done on lifecycle models, which has affected how software is created, nothing has really changed the way software is released to customers. The main obstacle has been technical—it's difficult to release small updates to customers after the initial release. The approach I present in this article lets you release an application, then enables applications or end users to download updates for selected portions of the application as updates become available. The examples I present that illustrate this approach were built and tested with JDK 1.4 and JDK 1.5. The final build was done with JDK 1.4, and should run under a 1.4 or 1.5 JVM. A distribution with the complete source, skins, and languages is available electronically; see "Resource Center," page 5. Even though the implementation is done in Java, this approach is language agnostic. The program that I developed when I was initially working on these problems (HalfMoon Calendar) was written in C++.

Say you are tasked with writing an application, but it needs a flexible look-and-feel and needs to support multiple languages, and your release date needs to be February 25, but you have 10 look-and-feel files, which will be released and tested as they become available, between February 1 and March 1, and you will need to release languages as translations come available.

Based on this, you create a set of requirements, and because we're programmers, gold plate the requirements a little:

  • Users need to be able to update a selected portion of the application (component) at any time. For this application, these components are languages and skins (for look-and-feel).
  • Although it may take some time for the user to download a component, after the download, the new component will perform like any other component of the same type.
  • Users should be able to refresh the list of available components at any time, and users should then be able to download and install updates using the same interface.

These are the requirements. The next task is to see if the requirements lead to a reasonable design, and whether that design leads to a solid reusable implementation.

User Experience

It's easier to understand the approach I present here if you can look at a sample application that implements the design. The sample application I use is a mortgage calculator that calculates conventional American mortgages and has functionality for loading skins and languages (both skins and languages will be delivered via incremental deployment). The interface is essentially the same for choosing skins and languages, so I only use one (skins, in this case) to explain the user experience.

[Click image to view at full size]

Figure 1: Clicking on the skins button causes the ComponentChooser to display a list of locally and remotely available skins.

[Click image to view at full size]

Figure 2: Boom skin has been loaded (after a transparent download from a remote server).

There is a skins button on the main interface. Clicking it brings up a dialog (Figure 1) that lets you choose a skin (double click on it in the list), then loads the skin that you chose (Figure 2). This is trivial until you consider two things:

  • The skin that the user is loading is not necessarily on their system. It may be loaded as needed from a remote system, then added to the local collection of skins when requested.
  • Users can refresh the list of skins, so the publisher can release new skins at anytime. Users can download skins by clicking refresh, then double clicking on the new skins. This same user experience applies to language, or to anything else that can be distributed as a component of an application (codecs, sounds files, data files, and the like).

System Design

There are three cases to be covered to meet the requirements:

  • The normal case where the system loads a component on startup.
  • The case where users want to get the most recent list of components.
  • The case where users select a component. (This case needs to handle components that are available on the local system as well as components that are not available on the local system.)

In terms of the design, applications have a list of components in a configuration file. These components have a filename and location (and potentially other attributes). When applications need to load components, they look first in the local cache (which is a directory in the filesystem). If the application does not find the component there, it then looks for it in a remote location. The URL of the remote location is kept in the configuration file. This configuration file, which contains the entire catalog of components that serve a given purpose (skins, languages, or whatever), can be refreshed either manually or automatically. The catalog is refreshed by downloading the catalog as a file from a remote server. So if publishers of the software have new skins, they upload the skins to their server, add the names of the skins and the URLs to the catalog file, then upload the catalog file to their server. The next time application users refresh their catalog, they will see the new components and can download them with a click.

[Click image to view at full size]

Figure 3: Diagram of the classes involved in component management for the sample application.

Figure 3 is the class diagram for the main classes involved in the managing components for the sample implementation. To understand Figure 3, the best place to start is ComponentInfo, the class that models the components. This class has information about the filename of the component, the location (which is the URL from which the component can be downloaded), and whether it is on the local filesystem (for instance, whether it has been downloaded in the past). The ComponentInfo list is created whenever users indicate that they want to choose a component, and represents the union of the components available on the local filesystem and those that are available for download. The ComponentInfo list is aggregated in the ComponentChooser, which uses the list to populate the list of components available (for example, the list of skins in Figure 1). If users select a component that is available on the local filesystem, it is loaded immediately. If users select a component that is available for download, ComponentChooser requests that the component be downloaded via RemoteComponentLoader. RemoteComponentLoader takes the location and filename from the ComponentInfo and downloads the remote object (for example, a skin) to the local cache (for a skin, this would be the skins directory), spooling it to a file with the filename for this component (for example, the Boom Realty skin can be downloaded from http://www.lithic.com/ddj/skins/boom.jar and spooled to [application root]/skins/boom.jar).

In a production system, it would be likely that you would ask users whether they want to download the remote component prior to actually downloading it, and you would likely have more attributes for the component; for example, size, version, user-friendly names, and so on.

With this design, if you want to add a new component, you would create a new Loader (a CodecLoader or whatever), create a catalog for that loader, and create a button or menu item to call that loader, which would in turn call the ComponentLoader. The remainder of the code should be completely reusable.

Implementation

To see if this design translates readily into code, the mortgage payment calculator example can load skins and languages. The idea is to demonstrate whether the aforementioned design is flexible enough to load any type of component, with only minor changes to the application.

The mortgage calculator has three basic functions: calculating mortgages, loading skins, and loading languages. Most of the scaffolding code for the application is in the SimpleMortgageCalculator.java file. In the constructor (Listing One), the widgets, buttons, labels, and text fields are created, then the application determines what skin and language the user was using last. The labels and buttons are created without any text, as the text is added after the language is loaded. After creating the widgets, the last skin is loaded, and information about the size of the skin and its background image is determined, then the skin layers the widgets on its surface.

SimpleMortgageCalculator() {
  super();
  this.setUndecorated(true);
  //this is where the settings from the last use are retrieved
  try{
    applicationProperties.load(new FileInputStream(CALCULATOR_CONFIG_FILE));
  }
  catch(Exception ex){
    System.out.println("problem loading properties 
                                     file on startup "+ex.toString());
  }
  // Add the window listener 
  addWindowListener(new WindowAdapter() {
    public void windowClosing(WindowEvent evt) {
      dispose(); 
      System.exit(0); 
  }});

  addMouseListener(this);
  addMouseMotionListener(this);

  //don't want this resizable, as it makes the background look odd
  this.setResizable(false);

  buttons=new Button[4];//calculate, skins, languages, quit
  fields=new TextField[4];//Amount, Interest Term, Length in years, Results
  labels=new Label[4];//Amount, Interest Term, Length in years, Results

  buttons[CALCULATE]=new Button("");//calculate
  buttons[SKINS]=new Button("");//skins
  buttons[LANGUAGES]=new Button("");//languages
  buttons[QUIT]=new Button("");//quit
  buttons[CALCULATE].addActionListener(this);
  buttons[SKINS].addActionListener(this);
  buttons[LANGUAGES].addActionListener(this);
  buttons[QUIT].addActionListener(this);
  this.add(buttons[CALCULATE]);
  this.add(buttons[SKINS]);
  this.add(buttons[LANGUAGES]);
  this.add(buttons[QUIT]);

  fields[AMOUNT]=new TextField(15);//amount
  fields[INTEREST]=new TextField(6);//interest
  fields[TERM]=new TextField(4);//term in years
  fields[RESULTS]=new TextField(10);//results
  this.add(fields[AMOUNT]);
  this.add(fields[INTEREST]);
  this.add(fields[TERM]);
  this.add(fields[RESULTS]);

  labels[AMOUNT]=new Label("");//amount
  labels[INTEREST]=new Label("");//interest
  labels[TERM]=new Label("");//terms
  labels[RESULTS]=new Label("");//results
  this.add(labels[AMOUNT]);
  this.add(labels[INTEREST]);
  this.add(labels[TERM]);
  this.add(labels[RESULTS]);

  //this is where the last skin and language are reloaded
  String lastSkin=applicationProperties.getProperty(LAST_SKIN_PROPERTY);
  String lastLanguage=
              applicationProperties.getProperty(LAST_LANGUAGE_PROPERTY);
  if(lastSkin==null) lastSkin="downtown.jar";
  if(lastLanguage==null) lastLanguage="english.properties";
  SkinLoader.getInstance().loadNewSkin(lastSkin);
  LanguageLoader.getInstance().loadNewLanguage(this, lastLanguage);
  Dimension d=SkinLoader.getInstance().getSize();
  setSize((int)d.getWidth(), (int)d.getHeight());
  //Center the window
  Dimension screenDim = Toolkit.getDefaultToolkit().getScreenSize();
  Rectangle winDim = getBounds();
  setLocation((screenDim.width - winDim.width) / 2,
         (screenDim.height - winDim.height) / 2);
  setVisible(true);
  //need a visible window to render the image onto.
  backgroundImage = SkinLoader.getInstance().getImage(this);
  SkinLoader.getInstance().addWidgets(this, buttons, fields, labels);
}
Listing One: SimpleMortgageCalculator.

The place where the users' actions are processed is the actionPerformed method (Listing Two), where the user's click on the Calculate, Skins, Languages, or Quit button calls the associated code. The Quit and Calculate buttons are pretty straightforward and not a core part of this article. The Skins and Languages buttons call the SkinLoader.chooseSkin and LanguageLoader.chooseLanguage, which (according to Figure 3 and the code) are one step away from where the ComponentChooser dialog is called. That's the core of this technology.

public void actionPerformed(ActionEvent e){
  if(e.getSource()==buttons[CALCULATE]){//calculate button
    double dResult=Calculate.crunch(fields[AMOUNT].getText(), 
                    fields[INTEREST].getText(), fields[TERM].getText());
    if(dResult>0.0){
      fields[RESULTS].setText(""+(int)dResult);
    }
    else{
      fields[RESULTS].setText(LanguageLoader.
                               getInstance().lookUpString("badresult"));
    }
  }
  else if(e.getSource()==buttons[SKINS]){//skins button
    SkinLoader.getInstance().chooseSkin(this); 

    //size is in the skin
    Dimension d=SkinLoader.getInstance().getSize();
    setSize((int)d.getWidth(), (int)d.getHeight());
    //as is the image
    backgroundImage = SkinLoader.getInstance().getImage(this);
    //and the display area    
    this.paint(this.getGraphics());
    //add controls
    SkinLoader.getInstance().addWidgets(this, buttons, fields, labels);
  }
  else if(e.getSource()==buttons[LANGUAGES]){//languages
    LanguageLoader.getInstance().chooseLanguage(this); 
  }
  else if(e.getSource()==buttons[QUIT]){//quit button
    try{
      //remember what the last skin and language were
      String lastSkin=SkinLoader.getInstance().getCurrentSkin();
      String lastLanguage=LanguageLoader.getInstance().getCurrentLanguage();
      applicationProperties.setProperty(LAST_SKIN_PROPERTY, lastSkin);
      applicationProperties.setProperty(LAST_LANGUAGE_PROPERTY, lastLanguage);
      applicationProperties.
              store(new FileOutputStream(CALCULATOR_CONFIG_FILE), 
                                  "SimpleMortageCaculator properties file");
    }
    catch(Exception ex){
      System.out.println("problem storing properties 
                                          file on shutdown "+ex.toString());
    } 
    //write out the configuration file, then quit
    exit();
  }
}
Listing Two: actionPerformed method.

Skins are loaded using the interface shown in Figure 1, which is generated by the ComponentChooser class. After the skin is selected (by double clicking the skin that you want to load in the list of skins), the ComponentChooser calls the SkinLoader loadSkin method (indirectly, via the loadComponent method in the ComponentLoader interface). The loadSkin method (see Listing Three) uses introspection to find the correct method to call.

  public void loadComponent(String skinFile){
    try{
      URL u = new URL("jar:file:skins/"+skinFile+"!/");
      int index=skinFile.indexOf(".jar");
      String thisClass=skinFile.substring(0, index);
      String strClass="skins."+thisClass;//jar and main class files match
      ucl = new URLClassLoader(new URL[] { u });
      c= (Class) Class.forName(strClass, true, ucl);
      m = c.getMethod(SKIN_METHOD, SKIN_METHOD_ARGS);
      currentSkin=skinFile;
      image=null;
    }
    catch(Exception ex){
      System.out.println("exception in SkinLoader.loadComponent()
"+ex.toString());
    }
  }
Listing Three: loadComponent method.

The reference that is obtained for the method, m, is cached, so subsequent calls to that method are very fast. In Windows, you would use LoadLibrary and GetProcAddress to do the same thing.

The unusual thing about this code is that a single method is loaded and cached, and then that method is used for all method calls. The method signature is skinMethod(Object [], Integer, Integer). The Object array holds all the arguments, the first Integer tells what type of call it is (getting the image name, the size of the skin, or for arranging the widgets), and the last Integer tells what version of the application is calling the skin. This permits forward and backward binary compatibility because the method signature never changes. For Windows, you could use a method signature of (void **, int, int, int), where the void ** serves the purpose of the Object [] in the Java call, and the first int indicates the number of elements in the void ** (not necessary in Java as you can get the length from the Object []). The second and third ints indicate type of call and version of caller, as with the Java code. One type of call that you would likely add, particularly if you are working in C, is a dumpSkin call, to deallocate any resources or memory the skin had obtained.

Listing Four is an example of the bundling that goes on in the SkinsLoader and Listing Five the unbundling that goes on in the skin. This bundling and unbundling is for getting the dimension of the skin, but a similar process is used for the other calls to the skin.

//bundling a dimension for a call to the skin to get the dimension
Object oa[]=new Object[1];
oa[0]=new Dimension();
Integer IReturn=(Integer)m.invoke(c, new Object[] {oa, new Integer(1), new Integer(1) });
d=(Dimension)oa[0];
Listing Four: Bundling in SkinsLoader.
//fragment from the skinMethod in the skin where arguments are unbundled
  public static Integer skinMethod(Object [] args, Integer callType, Integer callerVersion){
     
    int type=callType.intValue();

    switch(type){
      case 1://call for size of the window
        return new Integer(getSize((Dimension)args[0]));

//The getSize method in the skin sets the dimensions and then returns OK.

  private static int getSize(Dimension d){
    d.setSize(500, 300);
    return OK;
  }
Listing Five: Unbundling in the skin.

This use of dynamic loading and a single method makes binary compatibility easy to maintain, and also makes the deployment easy. I've used this method with Windows and Java and it works well on both platforms.

Languages

The mechanism for loading languages, from the user's point of view, is identical to loading skins. After the user chooses a language, the system either loads the language from the local cache or loads the language from the remote server, stashes it in the local cache, then loads the languages file into a Properties object. This Properties object is used for converting keys into localized text. Because this is a demonstration project, our strings don't cover all the strings used by the application, but they do cover the strings for the interface.

//code called in LanguageLoader to load the current language
  public static void loadNewLanguage(String languageFile){
    languageChangedFlag=true;
    properties.clear();
    try{
      properties.load(new FileInputStream("languages/"+languageFile));
    }
    catch(IOException ioe){
      System.out.println("problem loading language file "+ioe.toString());
    }
    frame.paint(frame.getGraphics());
  }
Listing Six: loadNewLanguage method.

The key method is loadNewLanguage (Listing Six), which is fed the name of the properties file to load. After loading, the application is repainted, during which the buttons and labels get their strings replaced. The replacement is done by looking up the strings in the properties file that was loaded. For example, the button text for the Quit button is looked up as:

LanguageLoader ll=
LanguageLoader.getInstance();
buttons[QUIT].setLabel(ll.lookUpString("quit"));

Downloading a Component

For components that are not available on the local filesystem, the application downloads them from the URL in the configuration file. When users launch the dialog that is used to select the component to load, the dialog box interface is created by the ComponentChooser class. Figure 4 is the interface for languages. The list of languages is created by the ListFiles class in the getComponents method. This method first reads in the properties file. An example entry from the skins.config properties file is:

realty.key=realty
realty.file=realty.jar
realty.location=
http://www.lithic.com/ddj/skins/realty.jar

[Click image to view at full size]

Figure 4: Language loading dialog, showing the currently chosen language of Spanish.

The key is a parsing aid, the file is the filename in the local cache (the skins directory), and the URL is where it can be downloaded if it is not in the local cache. The entries in the skins.config file are converted into ComponentInfo objects and added to a hash. After the properties are read in, the components in the filesystem are read and then added to the hash. Wherever there is a collision, the ComponentInfo is set to indicate that the component has already been downloaded. In this way, a list is created that indicates what is available locally and what is available remotely. This list is what is used to populate the list of components presented in the ComponentChooser dialog.

When users double click on a component, the filename is used to retrieve the related ComponentInfo object (Listing Seven). If the ComponentInfo object indicates that the component is not available on the local system, it is downloaded using the requestComponent method of the RemoteComponentLoader. On a production system, there should also be a request to the user to ensure that they agree to the download, plus a download progress indicator and more error handling for downloading problems.

if (e.getSource()==list) {
  // When the user double-clicks on a component, try to load it
  if (e.getID() == Event.ACTION_EVENT) {
    String strFile=list.getSelectedItem();
    ComponentInfo ci=(ComponentInfo)hashComponents.get(strFile);
      
    //if the component is not yet on the file system, download it
    if(!ci.getDownloaded()){
      this.setTitle("Downloading "+ci.getFileName());
      RemoteComponentLoader.requestComponent(ci.getLocation(), 
                                directory+"/"+ci.getFileName());
    }
    loader.loadComponent(strFile);  
    this.dispose();
  }
}
Listing Seven: Loading a component.

If you are behind a firewall, you will need to open up the calc.config file and set your proxy and proxyport.

After the download is complete, the ComponentChooser calls loader.loadComponent(strFile). The ComponentLoader (loader, in this case, either a SkinLoader or LanguageLoader) doesn't care whether the file was loaded from a remote server or the local filesystem, it just takes the filename and loads it. In a production system, users would be interacting with friendly names rather than filenames, so there would need to be a translation here from filename to friendly name.

Refreshing a Components Catalog

If the Refresh button of the ComponentChooser dialog is pressed (Figure 1), then a new catalog of components is downloaded and the list of components is refreshed. The code for this is straightforward:

RemoteComponentLoader.
requestComponent()
loader.getComponentCatalogUrl(),
loader.getComponentCatalogFile());
listComponents();

This code pulls down the new catalog, then repopulates the list of components; so any new components are now available for downloading by a double click.

Deployment Notes

If you decide to use this approach, there are a few considerations before deploying. I've been distributing software called HalfMoon Calendar for a few years and it uses this design, so these notes are from released production software.

First, determine where you are going to put your files on your server. In my case, I have an application where I have language files and skins, so I have a languages and skins directory with the update files (referred to as components elsewhere in this document) and the update catalog (for HalfMoon, the format for these are XML, rather than properties files). I hard code the URLs for the catalogs into my application, as I don't want users to change them, but if you want end users to be able change them, you can externalize them to a configuration file.

I can tell approximately how many people download HalfMoon as they often go to an online tutorial. More people download the skins catalog (the XML file with the skins list) than go to the tutorial, and most of them load some of the skins. This pattern indicates that the users find this an easy way to find the skins and download the ones that look interesting, and also would be a good way to make components such as codecs, parsers, or other application extensions available.

Cautions, Caveats, and Future Development

When I first wrote skins for HalfMoon Calendar, I used the approach presented here, where the skin was actually compiled code (although it was C code). The benefit is that you can add new functionality because you are writing in code and you have access to the main window's HWND (in the case of this Java application, you have access to its Frame). The drawback—and it is a big one—is that only programmers can write skins. I now think it is better to use properties files and images for skins, rather than compiled code (although either way works fine using this approach to incremental deployment). The way skins are created and accessed in this application, however, serves to demonstrate how to distribute loadable software modules.

In a production system, there would be a lot more attributes to describe each component, including a version, a friendly name (rather than the filename), and file size. Also, XML may be a better format for the information rather than a properties file, but either will work (using properties files saved me from cluttering things with parsing code). Further, for languages in a Java context, using ResourceBundles would be a more standard approach than the way languages are handled in this application.

DDJ


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.