The version of Apache log4j used by SoundHelix.

[[ 🗃 ^aEp6o apache-log4j ]] :: [📥 Inbox] [📤 Outbox] [🐤 Followers] [🤝 Collaborators] [🛠 Commits]

Clone

HTTPS: git clone https://vervis.peers.community/repos/aEp6o

SSH: git clone USERNAME@vervis.peers.community:aEp6o

Branches

Tags

v_1_1b5 :: docs /

deepExtension.html

<html>
<head>
<title>log4j extensions</title>
</head>

<body bgcolor="white">

<center>
<h1>Adding Conversion Characters to PatternLayout</h1>

  "Paul Glezen" 

  January 2001 <br><br>
</center>

<hr>
<h2>Abstract</h2>

<p>
This article describes a systematic way to extend the
<a href="http://jakarta.apache.org/log4j">log4j</a> API to include
additional attributes formatted using the
<code><a href="../docs/api/org/apache/log4j/PatternLayout.html">PatternLayout</a></code> 
class.
<p>
<hr>
<h2>Contents</h2>
<ul>
	<li><a href="#intro">Introduction</a>
	<ul>
		<li><a href=#case>The Case Study</a>
		<li><a href=#peek>A Peek Under the Hood</a>
	</ul>
	<li><a href="#process">The Process</a>
	<ul>
		<li><a href="#LoggingEvent">Extending <code>LoggingEvent</code></a>
		<li><a href="#PatternLayout">Extending <code>PatternLayout</code></a>
		<li><a href="#PatternParser">Extending <code>PatternParser</code> and 
			<code>PatternConverter</code></a>
		<li><a href="#Category">Extending <code>Category</code></a>
		<li><a href="#CategoryFactory">Extending <code>CategoryFactory</code></a>
	</ul>
	<li><a href="#usage">Usage</a>
	<ul>
		<li><a href="#configurators">Configurators</a>
	</ul>
	<li><a href="#enhancements">Further Enhancements</a>
</ul>
<p>
<hr>
<a name="intro"><h2>Introduction</h2></a>

<p>
This article assumes familiarity with the log4j
<a href="manual.html">User Manual</a>.  It builds on fundamental
classes described in both the User Manual and the
<a href="api/index.html" target="_top">Javadoc API</a>.  To assist in illustrating the
concepts, a simple case study will be developed along side the
explanations.  The resulting classes may be used as a template
for your own extensions.  Condenced (i.e. statements compressed,
comments removed) snipets of the case study code
are included in this document.

<a name="case"><h3>The Case Study</h3></a>
The case study was developed in a CORBA environment in which the
following information for each log entry was needed. The letters in
parenthesis represent the corresponding character to be used by the
<code><a href="api/org/apache/log4j/PatternLayout.html">PatternLayout</a></code> 
class for formatting.

<p>
<ul>
<li><b>Host Name (h)</b> - the IP address or hostname of the physical machine
	on which the Category was running.
<li><b>Server Name (s)</b> - The name of the application server process.  In
	this context, a <i>server</i> refers to a process that accepts requests
	rather than referring to a machine.  The term <i>host</i> refers to a
	physical machine.  Several servers may run on the same host.
<li><b>Component Name (b)</b> - Rather than getting bogged down on what
	constitutes a component, let's assume for the case study that a component
	is a unit of software worth denoting in the logs.
<li><b>Version (v)</b> - the version of the component from which the log entry
	originated.
</ul>
<p>
It seems odd to use "b" for the component name.  Presently
<code><a href="api/org/apache/log4j/PatternLayout.html">PatternLayout</a></code> 
already defines both "C" and "c" for class name and category name respectively.

<h3><a name="peek">A Peek Under the Hood</a></h3>
<p>
In principle, if the steps described below are followed closely, there is 
not a need to understand how the extended classes will be used by log4j.
But sometimes software development can be entirely unprincipled.  You may
wish to extend log4j in a different manner than describe here or you may
make a mistake that requires knowledge of what is really going on.  (Heaven
forbid there be a mistake in this document).  In any case, it doesn't hurt
to get an idea of what's going on.
<p>
The following describes a "typical" logging scenario in the un-extended log4j
case.
<p>
<ol>
<li>Application code invokes a log request on a
<code><a href="api/org/apache/log4j/Category.html">Category</a></code> 
object.  Let's say the <code>info</code> method was invoked.
<p>
<li>The first thing <code>info</code> does is to check if logging has
been turned off entirely for the info level.  If so, it returns
immediately.  We'll assume for this scenario that logging has not
been turned off for the info level.
<p>
<li>Next <code>info</code> compares the
<code><a href="api/org/apache/log4j/Priority.html">Priority</a></code> 
level for this category against <code>Priority.INFO</code>.  Assuming
the priority warrants logging the message, the category instantiates a
<code><a href="api/org/apache/log4j/spi/LoggingEvent.html">LoggingEvent</a></code> 
object populated with information available for logging.
<p>
<li>The <code>Category</code> instance passes the <code>LoggingEvent</code>
instance to all its 
<code><a href="api/org/apache/log4j/Appender.html">Appender</a></code> 
implementations.
<p>
<li>Most (but not all) <code>Appender</code> implementations should have 
an associated subclass of 
<code><a href="api/org/apache/log4j/Layout.html">Layout</a></code>.
The <code>Layout</code> subclass is passed the <code>LoggingEvent</code> 
instance and returns the event's information formatted in a 
<code>String</code> according to the configuration of the <code>Layout</code>.
<p>
When the <code>Layout</code> subclass is
<code><a href="api/org/apache/log4j/PatternLayout.html">PatternLayout</a></code>, 
the format of the event's information is determined by a character sequence
similar to the <b>C</b> language library's <code>printf</code> routine.
<code>PatternLayout</code> delegates the parsing of this character sequence to a
<code><a href="api/org/apache/log4j/helpers/PatternParser.html">PatternParser</a></code> 
instance.
<p>
When the <code>PatternLayout</code> was constructed, it created a 
<code>PatternParser</code> to tokenize the character sequence.  Upon 
recognizing a token, the <code>PatternParser</code> constructs an appropriate
<code><a href="api/org/apache/log4j/helpers/PatternConverter.html">PatternConverter</a></code> 
subclass, passing it formatting information from the token.  Often the
<code>PatternConverter</code> subclasses are implemented as static inner 
classes of <code>PatternParser</code>.  The <code>parse</code> method of
the <code>PatternParser</code> returns a linked list of these
<code>PatternConverter</code> subclasses.
<p>
<li><code>PatternLayout.format()</code> passes the <code>LoggingEvent</code>
to each <code>PatternConverter</code> subclass in the linked list.  Each link
in the list selects a particular item from the <code>LoggingEvent</code>,
converts this item to a <code>String</code> in the proper format and appends 
it to a <code>StringBuffer</code>.
<p>
<li>The <code>format</code> method returns the resulting <code>String</code>
to the <code>Appender</code> for output.
</ol>
<p>
The above discussing involved most of the classes that we must extend or
implement.
<p>
<ul>
<li><code><a href="api/org/apache/log4j/PatternLayout.html">org.apache.log4j.PatternLayout</a></code> 
<li><code><a href="api/org/apache/log4j/Category.html">org.apache.log4j.Category</a></code> 
<li><code><a href="api/org/apache/log4j/spi/CategoryFactory.html">org.apache.log4j.spi.CategoryFactory</a></code> 
<li><code><a href="api/org/apache/log4j/spi/LoggingEvent.html">org.apache.log4j.spi.LoggingEvent</a></code> 
<li><code><a href="api/org/apache/log4j/helpers/PatternParser.html">org.apache.log4j.helpers.PatternParser</a></code> 
<li><code><a href="api/org/apache/log4j/helpers/PatternConverter.html">org.apache.log4j.helpers.PatternConverter</a></code> 
</ul>
<p>
<hr>
<a name="process"><h2>The Process</h2></a>
Below are steps required to add additional attributes available
for logging by extending log4j.  This will allow you to specify
their output formats in the same manner as those provided by the
<code>PatternLayout</code> class.  The steps are numbered for
reference only.  It makes no difference in which order they are
followed.
<p>
It's helpful if you know the attributes you wish to add and a 
<code>PatternLayout</code> symbol for each one before you begin.  Be
sure to consult the <code>PatternLayout</code> documentation to ensure
the symbols you select are not already in use.
<p>
Before we dig in, I should give the standard lecture on comments.
If the log4j library were not well documented, it would be useless
to everyone but the log4j creators; likewise with your extensions.
Much like eating vegetables and saving the environment, we all agree
commenting code properly should be done.  Yet it is often sacrificed 
for more immediate pleasures.  We all write code faster without 
comments; especially those pesky Javadoc comments.  But the reality
is that the utility of undocumented code fades exponentially with time.
<p>
Since the log4j product comes with Javadoc comments together with
the documentation it produces, it makes sense to include Javadoc
comments in your extensions.  By their very nature, logging tools
are strong candidates for re-use.  They can only be independently
re-used if they are supported by strong documentation component.
<p>
This all having been said, I have elected to remove most comments from
examples in the interest of space rather than including them to serve
as a nagging reminder.  The reader is referred to the case study source 
code files for a Javadoc version and a
<a href="http://java.sun.com/javadoc">Javadoc website</a>
for more information on Javadoc conventions.

<a name="LoggingEvent"><h3>1. Extending <code>LoggingEvent</code></h3></a>

Extending the <code>LoggingEvent</code> class should be one of the
trivial steps.  All that is needed in the extension is the addition
of public data members representing the new attributes and a new
constructor to populate them.
<p>
<table border=1>
<tr><td>
<pre>
import org.apache.log4j.Category;
import org.apache.log4j.Priority;
import org.apache.log4j.spi.LoggingEvent;

public class AppServerLoggingEvent extends LoggingEvent
                                   implements java.io.Serializable 
{
   public String hostname;
   public String component;
   public String server;
   public String version;
   
   public AppServerLoggingEvent( String    fqnOfCategoryClass, 
                                 AppServerCategory  category, 
                                 Priority  priority, 
                                 Object    message, 
                                 Throwable throwable) 
   {
      super( fqnOfCategoryClass,
             category,
             priority,
             message,
             throwable );

      hostname  = category.getHostname();
      component = category.getComponent();
      server    = category.getServer();
      version   = category.getVersion();
   }  
}
</pre>
</table>
<p>
The constructor demonstrates that in most cases, the <code>Category</code>
subclass will contain most of the information necessary to populate
the attributes of the <code>LoggingEvent</code> subclass.  Extensions
to <code>LoggingEvent</code> seem no more than a collection of strings
with a constructor.  Most of the work is done by the super class.

<a name="PatternLayout"><h3>2. Extending <code>PatternLayout</code></h3></a>

Extending the <code>PatternLayout</code> class should be another
simple matter.  The extension to <code>PatternLayout</code> should 
differ from its parent only in the creation of a
<code>PatternParser</code> instance.  The extended
<code>PatternLayout</code> should create an extended
<code>PatternParser</code> class.  Fortunately, this task in
<code>PatternLayout</code> is encapsulated within a single method.
<p>
<table border=1> <tr><td>
<pre>
import org.apache.log4j.PatternParser;
import org.apache.log4j.PatternLayout;

public class AppServerPatternLayout extends PatternLayout 
{
   public AppServerPatternLayout() 
   {
      this(DEFAULT_CONVERSION_PATTERN);
   }

   public MyPatternLayout(String pattern) 
   {
      super(pattern);
   }
    
   public PatternParser createPatternParser(String pattern) 
   {
      PatternParser result;
      if ( pattern == null )
         result = new AppserverPatternParser( DEFAULT_CONVERSION_PATTERN );
      else
         result = new AppServerPatternParser ( pattern );

      return result;
  }
}
</pre>
</table>

<a name="PatternParser"><h3>3. Extend <code>PatternParser</code> and <code>PatternConverter</code></h3></a>

Recall from our <a href="#peek">peek under the hood</a> that the
<code>PatternParser</code> does much of its work in its
<code>parse</code> method.  The <code>PatternLayout</code> object
instantiates a <code>PatternParser</code> object by passing it
the pattern string.  The PatternLayout then invokes the
<code>parse</code> method of <code>PatternParser</code> to produce
a linked list of <code>PatternConverter</code> subclass instances.
It is this linked list of converters that is used to convert an
event instance into a string used by appenders.
<p>
Our job will be to subclass <code>PatternParser</code> to properly
interpret formatting characters we wish to add.  Fortunately,
<code>PatternParser</code> has been designed so that only the one
step in the parsing process differing for each formatting character
has to be overridden.  The grunt work of parsing is still performed
by the <code>PatternParser.parse()</code> method.  Only the
<code>PatternParser.finalizeConverter</code> method has to be
overridden.  This is the method that decides which
<code>PatternConverter</code> to create based on a formatting
character.
<p>
The extension to <code>PatternParser</code>, 
<code>AppServerPatternParser</code>, is similar to its super class.
It uses
<ul>
<li>constants to identify the various formats
<li>a converter defined as private static inner-classes of
	<code>AppServerPatternParser</code>.
<li>a <code>finalizeConverter</code> method which instantiates
	the appropriate converter for a given format character.
</ul>
<p>
<code>AppServerPatternParser</code> differs principally by 
dedicating a separate converter type for each logging
attribute to be formatted.
Rather than placing switch logic in the converter, like its
parent class, each converter only converts one format character.
This means the decision of which converter subclass
to instantiate is made at layout instantiation time rather
than in a switch statement at logging time.
<p>
It also differs in that the format constants are characters 
rather than integers.
<p>
<table border=1>
<tr><td>
<pre>
import org.apache.log4j.*;
import org.apache.log4j.helpers.FormattingInfo;
import org.apache.log4j.helpers.PatternConverter;
import org.apache.log4j.helpers.PatternParser;
import org.apache.log4j.spi.LoggingEvent;

public class AppServerPatternParser extends PatternParser 
{
   static final char HOSTNAME_CHAR  = 'h';
   static final char SERVER_CHAR    = 's';
   static final char COMPONENT_CHAR = 'b';
   static final char VERSION_CHAR   = 'v';

   public AppServerPatternParser(String pattern) 
   {
      super(pattern);
   }
    
   public void finalizeConverter(char formatChar) 
   {
      PatternConverter pc = null;
      switch( formatChar )
      {
         case HOSTNAME_CHAR:
            pc = new HostnamePatternConverter( formattingInfo );
            currentLiteral.setLength(0);
            addConverter( pc );
            break;
         case SERVER_CHAR:
            pc = new ServerPatternConverter( formattingInfo );
            currentLiteral.setLength(0);
            addConverter( pc );
            break;
         case COMPONENT_CHAR:
            pc = new ComponentPatternConverter( formattingInfo );
            currentLiteral.setLength(0);
            addConverter( pc );
            break;
         case VERSION_CHAR:
            pc = new VersionPatternConverter( formattingInfo );
            currentLiteral.setLength(0);
            addConverter( pc );
            break;
         default:
            super.finalizeConverter( formatChar );
      }
   }
  
   private static abstract class AppServerPatternConverter extends PatternConverter 
   {
      AppServerPatternConverter(FormattingInfo formattingInfo) 
      {
         super(formattingInfo);     
      }

      public String convert(LoggingEvent event) 
      {
         String result = null;
         AppServerLoggingEvent appEvent = null;

         if ( event instanceof AppServerLoggingEvent )
         {
            appEvent = (AppServerLoggingEvent) event;
            result = convert( appEvent );
         }
         return result;
      }

      public abstract String convert( AppServerLoggingEvent event );
   }

   private static class HostnamePatternConverter extends AppServerPatternConverter
   {
      HostnamePatternConverter( FormattingInfo formatInfo )
      {  super( formatInfo );  }

      public String convert( AppServerLoggingEvent event )
      {  return event.hostname;  }
   }

   private static class ServerPatternConverter extends AppServerPatternConverter
   {
      ServerPatternConverter( FormattingInfo formatInfo )
      {  super( formatInfo );  }

      public String convert( AppServerLoggingEvent event )
      {  return event.server;  }
   }

   private static class ComponentPatternConverter extends AppServerPatternConverter
   {
      ComponentPatternConverter( FormattingInfo formatInfo )
      {  super( formatInfo );  }

      public String convert( AppServerLoggingEvent event )
      {  return event.component;  }
   }

   private static class VersionPatternConverter extends AppServerPatternConverter
   {
      VersionPatternConverter( FormattingInfo formatInfo )
      {  super( formatInfo );  }

      public String convert( AppServerLoggingEvent event )
      {  return event.version;  }
   }
}
</pre>
</table>
<p>
<a name="Category"><h3>4. Extending <code>Category</code></h3></a>
Extending <code>Category</code> and its factory will be more straight 
forward than extending <code>PatternParser</code> and the converters.
The following tasks are involved in overridding 
<code>Category</code> for our purposes.
<p>
<ul>
<li>Add fields corresponding to the new attributes with their
	corresponding getters and setters.
<p>
<li>Override the constructor to accept or acquire information 
	about new attributes.
<p>
<li>Override the <code>forcedLog</code> method to ensure that a 
	correctly populated instance of 
	<code>AppServerLoggingEvent</code> is instantiated rather than
	the default <code>LoggingEvent</code>.
<p>
<li>Override the <code>getInstance</code> method to use our
	<code>CategoryFactory</code> (described in the next step).  This will
	require that we hold a static reference to our factory and provide a
	way to initialize it.
</ul>
<p>
Most of the code below is standard getter/setter verbage which has been
somewhat abbreviated.  The notable parts are in bold.  We add five more
attributes to <code>Category</code>: the four new logging attributes
plus a static <code>AppServerCategoryFactory</code> reference.  This is
pre-initialized to an instance with attributes set to null as a
precautionary measure.  Otherwise the <code>getInstance</code> method
will result in a null pointer exception if invoked before the
<code>setFactory</code> method.
<p>
The <code>getInstance</code> method simply invokes its parent class
method that accepts a <code>CategoryFactory</code> reference in
addition to the category name.
<p>
The <code>forcedLog</code> method follows closely the corresponding
parent class method.  The most important difference is the instantiation
of the <code>AppServerLoggingEvent</code>.  A minor yet necessary 
difference is the use of the <code>getRendererMap()</code> method rather
than accessing the data member directory as in <code>Category</code>.
<code>Category</code> can do this because the rendererMap 
is package level accessible.
<p>
The <code>setFactory</code> method is provided to allow application code 
to set the factory used in the <code>getInstance</code> method.
<p>
<table border=1>
<tr><td>
<pre>
import org.apache.log4j.Priority;
import org.apache.log4j.Category;
import org.apache.log4j.spi.CategoryFactory;
import org.apache.log4j.spi.LoggingEvent;

public class AppServerCategory extends Category
{
   protected String <b>component</b>;
   protected String <b>hostname</b>;
   protected String <b>server</b>;
   protected String <b>version</b>;
   private static CategoryFactory <b>factory = 
                  new AppServerCategoryFactory(null, null, null)</b>;

   protected  AppServerCategory( String categoryName,
                                 String hostname,
                                 String server,
                                 String component,
                                 String version )
   {
      super( categoryName );
      <b>instanceFQN = "org.apache.log4j.examples.appserver.AppServerCategory";</b>
      
      this.hostname  = hostname;
      this.server    = server;
      this.component = component;
      this.version   = version;
   }

   public String getComponent()
   { return (component == null ) ? "" : result; }

   public String getHostname()
   {  return ( hostname == null ) ? "" : hostname; }

   public static Category <b>getInstance</b>(String name)
   {
      return Category.getInstance(name, factory);
   }

   public String getServer()
   {  return ( server == null ) ? "" : server; }

   public String getVersion()
   {  return ( version == null ) ? "" : version; }

   protected void <b>forcedLog</b>( String    fqn, 
                             Priority  priority, 
                             Object    message, 
                             Throwable t) 
   {
<b>      LoggingEvent event = new AppServerLoggingEvent(fqn, this, priority, message, t);</b>
      callAppenders( event );
   }

   public void setComponent(String componentName)
   { component = componentName; }

   public static void <b>setFactory</b>(CategoryFactory factory)
   { AppServerCategory.factory = factory; }

   public void setHostname(String hostname)
   { this.hostname = hostname; }

   public void setServer(String serverName)
   { server = serverName; }

   public void setVersion(String versionName)
   { version = versionName; }
}
</pre>
</table>
<p>

<a name="CategoryFactory"><h3>5. Extending <code>CategoryFactory</code></h3></a>

The last step is to provide an implementation of the 
<code>CategoryFactory</code> interface that will correctly 
instantiate our <code>AppServerCategory</code> objects.  It
will obtain the hostname of the machine on which it runs using the
<code>java.net</code> API.  Aside from providing getters and
setters for the attributes introduced, the only method to
be implemented is the <code>makeNewCategoryInstance</code>.
<p>
Below is a snipet from <code>AppServerCategoryFactory</code>
with getters, setters and comments removed.
<p>
<table border=1>
<tr><td>
<pre>
import org.apache.log4j.Category;
import org.apache.log4j.spi.CategoryFactory;
import java.net.InetAddress;
import java.net.UnknownHostException;

public class AppServerCategoryFactory implements CategoryFactory
{
   protected String hostname;
   protected String server;
   protected String component;
   protected String version;
   protected ResourceBundle messageBundle;

   protected  AppServerCategoryFactory( String serverName,
                                        String componentName,
                                        String versionName )
   {
      try
      {
         hostname = java.net.InetAddress.getLocalHost().getHostName();
      }
      catch ( java.net.UnknownHostException uhe )
      {
         System.err.println("Could not determine local hostname.");
      }

      server    = serverName;
      component = componentName;
      version   = versionName;
   }

   public Category makeNewCategoryInstance(String name)
   {
       Category result = new AppServerCategory( name, 
                                                hostname, 
                                                server, 
                                                component, 
                                                version);
       return result;
   }
}
</pre>
</table>
<p>
<hr>
<a name="usage"><h2>Usage</h2></a>
We now arrive at how to use what we have created.  We must remember to
initialize log4j by creating an instance of 
<code>AppServerCategoryFactory</code> and passing it to 
<code>AppServerCategory</code>.  Once done, we can obtain a
<code>AppServerCategoryInstance</code> anytime by using the static
<code>getInstance</code> method of <code>AppServerCategory</code>.
This will ensure that <code>AppServerLoggingEvent</code> instances
are generated by the category logging methods.
<p>
<table border=1>
<tr><td>
<pre>
import org.apache.log4j.*;
import org.apache.log4j.appserver.AppServerCategory;
import org.apache.log4j.appserver.AppServerCategoryFactory;
import org.apache.log4j.appserver.AppServerPatternLayout;

public class test
{
   private static String formatString = 
      "---------------------------------------------------%n" +
      "Time:      %d%n" +
      "Host:      %h%n" +
      "Server:    %s%n" +
      "Component: %b%n" +
      "Version:   %v%n" +
      "Priority:  %p%n" +
      "Thread Id: %t%n" +
      "Context:   %x%n" +
      "Message:   %m%n";

   public static void main(String[] args)
   {
      AppServerCategoryFactory factory;
      factory = new AppServerCategoryFactory("MyServer", "MyComponent", "1.0");
      AppServerCategory.setFactory( factory );
      Category cat = AppServerCategory.getInstance("some.cat");

      PatternLayout layout = new AppServerPatternLayout( formatString );
      cat.addAppender( new FileAppender( layout, System.out) );

      cat.debug("This is a debug statement.");
      cat.info("This is an info statement.");
      cat.warn("This is a warning statement.");
      cat.error("This is an error statement.");
      cat.fatal("This is a fatal statement.");
   }
}
</pre>
</table>
<a name="configurators"><h3>Configurators</h3></a>
There is one a word of caution concerning the use of configurators that
may create <code>Category</code> instances (such as
<a href="api/org/apache/log4j/PropertyConfigurator.html">
<code>PropertyConfigurator</code></a> 
and 
<a href="api/org/apache/log4j/xml/DOMConfigurator.html">
<code>DOMConfigurator</code></a>).  Since these configurators do not
know about our extensions, any <code>Category</code> instances they 
create will not be <code>AppServerCategory</code> instances.  To
prevent this problem, any <code>AppServerCategory</code> that one
might want to be configured through a configurator should be
instantiated before the configure method is invoked.  In this way,
the configurator will configure the <code>AppServerCategory</code>
that already exists rather than creating an instance of its super
class.
<p>
The consequence of a configurator creating the super class by
mistake is merely that the extra attributes will not appear in 
the log output.  All other attributes are conveyed properly.
<p>
<hr>
<a name="enhancements"><h2>Further Enhancements</h2></a>
There are some other directions in which this log4j extension 
may be enhanced.
<p>
<ol>
<li>The hostname attribute could incorportate a formatting convention
	similar to that of class and category names whereby only a certain
	number of the more significant components are displayed.  But
	whereas with class and category names, the most significant component
	is on the right, with host names, it is on the left.
<p>
<li>Specifying a version number could be dangerous since programmers
	are apt to change versions of the code without changing the 
	string constant in the code which specifies the version.  Some
	source control programs provide for insertion of a version number
	into source.  For those that don't, including the version number as
	a constant is likely to lead to confusion later on.  It would be
	nice to see this short-coming addressed.
</body>
</html>

[See repo JSON]