Tuesday, January 23, 2007

Converting OPML files with XSLT

We just got some nice new Dell laptops here at Magenic. Each laptop comes with IE7 and Office 2007 (Word 2007, Outlook 2007, and so on). IE7 has the capability to track RSS feeds. Our Minneapolis GM, Dave Meier was using Onfolio to track his RSS feeds and decided to switch to IE7. In theory, this should be as simple as exporting the OPML file that describes the feeds from Onfolio and importing that file into IE7. There is a standard for these files (see http://www.opml.org/spec2#subscriptionLists). However, it turns out that Onfolio was not outputting an attribute “required” by the standard, specifically type=”rss”. It seems that there are packages that just assume that RSS is the target and leave this out. IE7 was enforcing the requirement that the attribute be present (Microsoft enforcing standards, who knew?).

Obviously, one could edit the OPML file in a text editor and fix it manually. This actually would not be too bad with a macro capability that could loop through the text and make the changes, but what would be the fun in that. Dave Meier was aware of XSLT and thought that it might be of help. For those of you not familiar with XSLT, it is a language (expressed in XML) that defines a set of transformations from an input XML file to an output file. The output file may be XML (as you will see is the case here), an HTML file, or a Text file. XSLT is very much a pattern matching language: the language says “when you see this pattern in the XML input, do these things to create output”.

Let’s take a look at the example that I created for Dave Meier. The input looked something like this:

<opml version="1.1">

<head>

<title>My Feeds</title>

</head>

<body>

<outline text="News">

<outline text="Latest news from Minneapolis/St. Paul Business Journal" xmlUrl="http://www.bizjournals.com/rss/feed/daily/twincities" htmlUrl="http://twincities.bizjournals.com/twincities/breaking_news.html?from_rss=1" />

</outline>

</body>

</opml>


The issue here that complicates things is that there are two “outline” elements at different levels of the hierarchy. The higher (or grouping) element is distinguished from the lower (or specifying) element by the fact that only the lower element has the “xmlUrl” attribute.

However, with XSLT this is not a problem. Here is the XSLT “script” (XSLT is typically compiled, making it a “programming” language but I have typically used XSLT in scripting situations which explains why I call it a script):

 


<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:output method="xml" encoding="UTF-8" omit-xml-declaration="yes" indent="no"/>
<xsl:template match="@*|node()">
<xsl:copy>
<xsl:apply-templates select="@*|node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="outline[@xmlUrl]">
<outline>
<xsl:attribute name="type">rss</xsl:attribute>
<xsl:apply-templates select="@*|node()"/>
</outline>
</xsl:template>
</xsl:stylesheet>

This script contains two templates. The first template is a general template that matches everything in the incoming XML file. This template is a very common template that appears in many XSLT scripts. It is called the “identity” template in that it copies everything in the input to the output. The second template is specific to our application. It matches to those elements that have an “xmlUrl” attribute. The output actions within this template output the “outline” element, the missing “type” attribute, and the rest of the matched “outline” element. We could test for the presence of an existing type attribute but XSLT is smart enough to simply replace the value of the attribute if it already exists. In this case, in effect, the script will keep any existing values and add the “rss” value only if there is no existing “type” attribute.

A question of interest is “what happens when two or more templates match a particular node of the input?” The answer is that the XSLT processor/compiler assigns a priority to each template based upon how widely the template in question “spreads its pattern matching net”. The narrower the scope of the match, the higher priority assigned to that the template. The wider the scope of the match, the lower priority assigned to that template. There are some more rules to assigning priority, but in the end when multiple templates match the incoming node in the XML file, the template with the highest priority is applied. What this means in our example, is that the upper “identity” template matches everything but the input matched by the lower “application” template. In other words, most applications of XSLT consist of dropping the identity template into the script and adding just the templates needed to address the specific input that needs to be changed.

If you have an XSLT processor handy, you just invoke that processor specifying the input XML, the script, and the output XML file. There are several free processors available, most of which use Java. Since we are a Microsoft shop, you should know that .NET contains a class in the BCL that handles XSLT 1.0 (there is an XSLT 2.0 standard but Microsoft has chosen so far not to support it). Here is the .NET C# source code for a simple command line utility to transform an input file using an XSLT script to produce an output XML file (watch for word wrap):

 


1 using System;
2 using System.IO;
3 using System.Xml;
4 using System.Xml.Xsl;
5 using System.Text;
6
7 namespace RunXsltTransformation
8 {
9
10 class MainProgram
11 {
12
13 private static FileInfo InputXML;
14 private static FileInfo XsltXML;
15 private static FileInfo OutputXML;
16
17 static void Main(string[] args)
18 {
19
20 bool CanContinue = true;
21
22 if(CanContinue)
23 {
24 CanContinue = PrepareFiles(args);
25 }
26
27 if (CanContinue)
28 {
29 CanContinue = TransformInput();
30 }
31
32 if (CanContinue)
33 {
34 Console.WriteLine("Completed Successfully");
35 }
36 else
37 {
38 Console.WriteLine("Could not continue because of errors");
39 }
40
41 }
42 private static bool PrepareFiles(string[] args)
43 {
44 try
45 {
46 if (3 != args.Length)
47 {
48 WriteMessage("There must be three arguments: input file path, Xslt file path, output file path");
49 return false;
50 }
51 InputXML = GetIncomingFileInfo(args[0], "Input XML");
52 XsltXML = GetIncomingFileInfo(args[1], "XSLT XML");
53 OutputXML = GetOutgoingFileInfo(args[2], "Output XML");
54 if (null == InputXML || null == XsltXML || null == OutputXML)
55 {
56 return false;
57 }
58 return true;
59 }
60 catch (Exception ex)
61 {
62 WriteMessage(ex.Message);
63 return false;
64 }
65 }
66 private static bool TransformInput()
67 {
68 XmlReader InputReader = null;
69 StreamWriter OutputWriter = null;
70 try
71 {
72 // build the transform
73 XslCompiledTransform transform = new XslCompiledTransform();
74 InputReader = new XmlTextReader(new StreamReader(InputXML.FullName, System.Text.Encoding.UTF8));
75 OutputWriter = new StreamWriter(OutputXML.FullName, false, System.Text.Encoding.UTF8);
76 transform.Load(XsltXML.FullName, XsltSettings.Default, null);
77
78 // and now the transform
79 transform.Transform(InputReader, null, OutputWriter);
80 return true;
81 }
82 catch (Exception ex)
83 {
84 WriteMessage(ex.Message);
85 return false;
86 }
87 finally
88 {
89 InputReader.Close();
90 OutputWriter.Close();
91 }
92 }
93 private static FileInfo GetIncomingFileInfo(string theFilePath, string theTitle)
94 {
95 if (null == theFilePath)
96 {
97 WriteMessage(theTitle + " file path cannot be null");
98 return null;
99 }
100 if (theFilePath.Trim().Length == 0)
101 {
102 WriteMessage(theTitle + " file path cannot be empty");
103 return null;
104 }
105 if (false == File.Exists(theFilePath))
106 {
107 WriteMessage(theTitle + " file path must exist");
108 return null;
109 }
110 return new FileInfo(theFilePath);
111 }
112 private static FileInfo GetOutgoingFileInfo(string theFilePath, string theTitle)
113 {
114 if (null == theFilePath)
115 {
116 WriteMessage(theTitle + " file path cannot be null");
117 return null;
118 }
119 if (theFilePath.Trim().Length == 0)
120 {
121 WriteMessage(theTitle + " file path cannot be empty");
122 return null;
123 }
124 if (true == File.Exists(theFilePath))
125 {
126 File.Delete(theFilePath);
127 }
128 return new FileInfo(theFilePath);
129 }
130 private static void WriteMessage(string theMessage)
131 {
132 Console.WriteLine(theMessage);
133 }
134
135 }
136 }
137

This is taken from a VS2005 project that compiles the binary into a file called RunXsltTransformation.exe. With that binary, the call to convert the feeds file, myFeeds.xml, to an output file, myConvertedFeeds.xml, using an XSLT script, ConvertOPML.xslt, would look like this (running from the command line in the same directory where all of the files are located):

RunXsltTransformation myFeeds.xml ConvertOPML.xslt myConvertedFeeds.xml

Sunday, September 17, 2006

The Passion of the Termination Process

As I have mentioned before in this blog, I have had only a few jobs. That is why I can't really say that I have a wealth of knowledge and experience with the termination process. However, I am quite surprised by the non-event that my recent termination from my last employer has been.

I expected to be deluged with bunches of different papers to sign but the termination process is self-service (like everything else these days).  You go to a corporate web site and click on a few options and out pops everything I ever needed to know about termination.  There have been no papers to sign. There has been no exit interview. The final check arrives in the mail. There is no goodbye luncheon; my boss lives 1000 miles away from me and besides he was on vacation the week that I was scheduled to spend my last day with the corporation.  Our last communication was through instant messaging.   I had to turn in my laptop, my badge and my corporate credit card to one of my coworkers.  I wasn't expecting a brass band or a massive severance check but I was there for 6 1/2 years and did a reasonable job (if you believed my annual reviews).  It would've been nice to have a thank you for my time.

What really surprises me is that the HR people wouldn't want to have some kind of a exit interview. Think of the data they could plug into their models; think of all the various plots of data points that they can produce; think of all the PowerPoint presentations that could be generated from that data.

But I guess it doesn't matter anymore. We have become a disposable society and that impacts even large corporations. We are all expendable. That is really kind of sad that we are reduced to such a passionless process.

Sunday, September 10, 2006

Rhythms

Change is hard. I have rhythms in my life. I figure out how to do something and find a place for that thing in the pattern of my life. When things change, I have to go back and reestablish all the rhythms. That takes a while and somehow seems to be a monumental waste of time: unavoidable but unpleasant. I am changing jobs and that drives a whole bunch of changes in my life: preparing to setup a new a new laptop, commuting locally rather than getting on a plane, selecting a new set of benefits, and on and on. To add to the confusion, my wife has also taken a new job recently: a better job but still a new job with different patterns.

One thing that I have to do is figure out where the time to write this blog fits in. One of the things that I am going to try is to dictate the rough drafts while I commute. I live a ways out from everything: lovely place to live but 30 or more minutes from all of the interesting places to be. I use Dragon Naturally Speaking Version 9 for a lot of my writing. I dictate into a headset microphone and the words just appear. It is not 100% but the speed of getting the rough material into place is great. One of the options that Naturally Speaking provides is the ability to transcribe material from a digital recorder. I just bought a WS100 digital recorder from Olympus and created a user that captures the peculiarities of my voice recorded on the digital recorder through the headset microphone. It may take a few weeks to work out the kinks but that is what life is for.

Wednesday, August 30, 2006

Abstract Dancer

Just as I did with interfaces before evaluating whether interfaces were the way to go with the implementation of the data value objects, I thought I'd talk about abstract classes. Again, I realized that I had quite a lot to say, more than would fit into a single entry in this blog. What I thought that I would do would be to list the topics in this blog entry that I intend to talk about. Later, as I finished each of these separate blog entries, I would come back and update this blog entry to include the links to those individual blog entries. And now on with the show:

  • The Dancer Becomes the Dance. A discussion of abstract classes in general terms using dance as a metaphor.
  • You Can’t Get There from Here. An examination of the problems with abstract classes with some more use of the dance metaphor.
  • There are many paths to the Top of the Mountain. Some material on guidelines and suggestions about how to improve the use of abstract classes.

tags: , , ,

Sunday, August 27, 2006

Redemption! Oh Sweet Redemption!

In this entry I am going to comment on the revised document interface.  I have already posted the code for the interface as well as the three classes that the revised interface uses: ReturnStatus, DocumentIndex, and TOCElement.  I am including these as separate entries so that you can have multiple web pages up at the same time.  

 The documentation included in these three classes should go a long way toward helping you to understand what is going on in this revised interface.  However, there are a number of topics that I think need some additional commentary.

First, I've tried as much is possible to substitute specific classes for generic value types.  For example rather than using a string class for the document index, I've created an actual document index class.  As presented, the document index class is simply a wrapper around a string.  If you just look at the executing code, this might look a bit silly.  But if you look at the code as a way of expressing the intent, as a way of communicating to people who follow in your footsteps and look at this code in an attempt to understand it, this works quite well in my mind.  I've done the same thing with the table of contents entry.  There's a little bit more information in this class but my primary motivation for creating the class was to more clearly express the intent of the design.

Second, I've tried to reduce the number of methods and to eliminate any sequencing dependencies between the various methods.  The way that I have done this is to introduce to delegates, one to process an individual table of contents entry and a second to process a paragraph within a specific portion of the document.  There are two corresponding methods that work through the document and invoke these delegates at the corporate point.  In my experience, this is a much less error prone approach.  It does push some of the logic into the interface but the variability in how the data is stored within the individual document justifies the increased responsibility.

Third, I have created a separate class to hold the return status.  It is a personal preference, but I do not like "out" parameters.  I much prefer to have everything come back as a returned result of the function.  This eliminates much of the need for the interface to hold state between calls.  Everything that you could ever want to know, is wrapped up in the return value.  if you need more data to be returned, you can subclass the return status class to add this material.

The actual production-quality document manager interface would be more complicated.  The intent here is not to give you an implementation but rather to point out some of the ways that you can use interface more effectively.

The Revised Document Management Interface

This is the revised document management interface.  I am including this as a separate entry so that you can have multiple web pages up at the same time.  This interface uses three other class: ReturnStatus, DocumentIndex, and TOCElement.  My commentary on this revised interface will be posted shortly.  By the way, I am posting this entry using the beta version of Live Writer using the Live Writer plug-in for code formatting written by Steve Dunn.  Works quite nicely.

namespace Interfaces
{

/// <summary>
/// This is the delegate that can be used to process
/// an individual TOC entry.
/// </summary>
/// <param name="tocElement">An entry holding data from
/// the table of contents.</param>
/// <returns>An instance of the <see cref="ReturnStatus"/>
/// class that holds the success/failure indicator
/// and an error message for this invocation of the delegate;
/// if the last operation succeeded,
/// the error message is an empty string.
/// In general, the logic of the delegate should return errors
/// in the return status rather than throwing exceptions.
/// </returns>
public delegate ReturnStatus ProcessTOCElement(TOCElement toc);

/// <summary>
/// This is the delegate that can be used to process paragraphs
/// with the portion of the document that is referened by an
/// instance of the <see cref="DocumentIndex"/> class.
/// </summary>
/// <param name="documentIndex">An instance of a
/// <see cref="DocumentIndex"/> class that "points" to the
/// the portion of the document that holds the contents of
/// the <see cref="paragraph"/> parameter.
/// </param>
/// <param name="paragraph">An string holding the text of the
/// paragraph in Unicode format without any formatting
/// indicators.</param>
/// <param name="paragraphNumber">An integer that holds
/// the relative position of the paragraph within the portion
/// of the document referenced by the <see cref="documentIndex"/>
/// parameter.
/// </param>
/// <returns>An instance of the <see cref="ReturnStatus"/>
/// class that holds the success/failure indicator
/// and an error message for this invocation of the delegate;
/// if the last operation succeeded,
/// the error message is an empty string. In general,
/// the logic of the delegate should return values in the
/// return status rather than throwing exceptions.</returns>
public delegate ReturnStatus ProcessParagraph
(DocumentIndex documentIndex,
int paragraphNumber,
string paragraph);

/// <summary>
/// Defines the actions that the document interface/viewer
/// can take against a specified portion of the document.
/// </summary>
public enum DocumentActions
{
/// <summary>
/// Display the portion of the document
/// </summary>
Display,
/// <summary>
/// Print the portion of the document
/// </summary>
Print,
/// <summary>
/// Transmit a copy of the portion of the document
/// </summary>
Transmit,
/// <summary>
/// Store a copy of the portion of the document in a file
/// or other persistent store
/// </summary>
Store
}
/// <summary>
/// This is a revision of the document management interface.
/// </summary>
public interface IManageDocumentsFixed
{
/// <summary>
/// Prepare the document for use.
/// </summary>
/// <exception cref="InvalidOperationException">
/// When the client software opens an already open document.
/// </exception>
/// <returns>An instance of the <see cref="ReturnStatus"/>
/// class that holds the success/failure indicator
/// and an error message for the open operation;
/// if the open operation succeeded, the error message
/// is an empty string.</returns>
/// <remarks>The location of the document is established in
/// the constructor of the class that
/// implements this interface. It is
/// </remarks>
ReturnStatus Open();

/// <summary>
/// Retrieve the title of the document.
/// </summary>
/// <returns>The overall title of the document.</returns>
string GetTitle();

/// <summary>
/// Release the document. The document must be re-opened
/// again before any further use.
/// </summary>
/// <exception cref="InvalidOperationException">
/// When the client software closes a document that is not
/// open.
/// </exception>
/// <returns>An instance of the <see cref="ReturnStatus"/>
/// class that holds the success/failure indicator
/// and an error message for the close operation;
/// if the close operation succeeded,
/// the error message is an empty string.</returns>
ReturnStatus Close();

/// <summary>
/// Returns the reference to the "root" element
/// of the document.
/// </summary>
/// <returns>An instance of the
/// <see cref="DocumentIndex"/> class that
/// refers to the "root" element of the document.</returns>
DocumentIndex GetRootIndex();

/// <summary>
/// Run through each of the entries in the table of contents
/// in a depth-first fashion by invoking the specified
/// <see cref="ProcessTOCElement"/> delegate for each entry.
/// Each TOCElement contains the document index element
/// that can be used to position the document to a
/// specific location within the document; the title
/// of the TOC entry; and the hierarchical level of the
/// TOCElement within the table of contents.
/// </summary>
/// <param name="localRoot">An instance of a
/// <see cref="DocumentIndex"/> class that "points" to the
/// the starting point for the run through. The client
/// should use the return value from the
/// <see cref="GetRootIndex"/> method to start at the root
/// of the document.
/// </param>
/// <param name="process">A reference to a delegate that
/// conforms to the <see cref="ProcessTOCElement"/> definition.
/// </param>
/// <param name="maximumLevel">The maximum number of levels
/// to descend. The value must be non-negative. A value of
/// zero will invoke the delegate just once for the root;
/// A value of one will invoke the delegate once for the
/// root followed by once for each direct child of the root.
/// And so on.
/// </param>
/// <exception cref="ArgumentNullException">
/// When the localRoot parameter is null.</exception>
/// <exception cref="InvalidOperationException">
/// When the localRoot parameter "points at"
/// a portion of the document that does not exist.
/// </exception>
/// <exception cref="ArgumentOutOfRangeException">
/// When the maximum level parameter is less than zero.
/// </exception>
/// <exception cref="ArgumentNullException">
/// When the process parameter is null.</exception>
/// <returns>An instance of the <see cref="ReturnStatus"/>
/// class that hold the overall success/failure indicator
/// and any error messages accumulated during the process;
/// if the operation succeeded,
/// the error message is an empty string.
/// Note that the return value is
/// the summation of all of the return values from
/// each of the invocations of the delegate.</returns>
/// <remarks>This method must be safe for recursion,
/// that is, the client program may call this method
/// to process each of the chapters. The invocation of the
/// delegate can do some process that includes a call
/// to <see cref="ProcessTOC"/> method, this time to process
/// the sections within the chapter. The logic of this
/// method must also not be affected by any use of the
/// <see cref="ProcessParagraph"/> method.</remarks>
ReturnStatus ProcessTOC(DocumentIndex localRoot,
ProcessTOCElement process,
int maximumLevel);

/// <summary>
/// Run through each of the paragraphs in the specified
/// section of the document by invoking the specified
/// <see cref="ProcessParagraph"/> delegate
/// for each paragraph.
/// </summary>
/// <param name="localRoot">An instance of a
/// <see cref="DocumentIndex"/> class that "points" to the
/// the portion of the document that holds the paragraphs
/// to be the run through. The client should use the return
/// value from the <see cref="GetRootIndex"/> method
/// to process the root of the document.
/// </param>
/// <param name="process">A reference to a delegate that
/// conforms to the <see cref="ProcessParagraph"/> definition.
/// </param>
/// <exception cref="ArgumentNullException">
/// When the localRoot parameter is null.</exception>
/// <exception cref="InvalidOperationException">
/// When the localRoot parameter "points at"
/// a portion of the document that does not exist.
/// </exception>
/// <exception cref="ArgumentNullException">
/// When the process parameter is null.</exception>
/// <returns>An instance of the <see cref="ReturnStatus"/>
/// class that hold the overall success/failure indicator
/// and any error messages accumulated during the process;
/// if the operation succeeded,
/// the error message is an empty string.
/// Note that the return value is the summation of all
/// of the return values from each of the
/// invocations of the delegate.</returns>
/// <remarks>This method is not intended to provide
/// significant display capabilities. That work should be
/// done through the viewer for the document controlled by
/// this interface. This method is intened to provide brief
/// "teaser" exerpts that can assist the user in selecting
/// a given paragraph.</remarks>
ReturnStatus ProcessSectionParagraphs(DocumentIndex localRoot,
ProcessParagraph process);

/// <summary>
/// Perform the specified action on the document at the
/// specified location within the document.
/// </summary>
/// <param name="action">A value taken from the
/// <see cref="DocumentActions"/> enumeration that
/// defines the action that the document viewer controlled
/// by this interface should take.</param>
/// <param name="documentIndex">A string value returned
/// by the <see cref="GetCurrentTOCID"/> method
/// or one of the values
/// returned by the <see cref="GetIndices"/> method.</param>
/// <exception cref="ArgOutOfRangeException">
/// When the action parameter has a value that is not found
/// in the <see cref="DocumentActions"/> enumeration.
/// </exception>
/// <exception cref="ArgumentNullException">
/// When the localRoot parameter is null.</exception>
/// <exception cref="InvalidOperationException">
/// When the localRoot parameter "points at"
/// a portion of the document that does not exist.
/// </exception>
/// <returns>An instance of the <see cref="ReturnStatus"/>
/// class that hold the overall success/failure indicator
/// and any error messages accumulated during the process;
/// if the operation succeeded,
/// the error message is an empty string.
/// Note that the return value is the summation of all
/// of the return values from each of the
/// invocations of the delegate.</returns>
ReturnStatus ActAtIndexLocation
(DocumentActions action, DocumentIndex documentIndex);

/// <summary>
/// Retrieve a set of zero or more document indices
/// that represent the locations within the document
/// where the lookup parameter value appears.
/// </summary>
/// <param name="lookup">The "index term" to search for.</param>
/// <returns>An array of document indices. If the search term
/// does not match any part of the document, the returned
/// array is zero length.</returns>
DocumentIndex[] GetIndices(string lookup);

}
}

The TOCElement Class

This class is one of several classes that I use in the revised document management interface.  I am including them as separate entries so that you can have multiple web pages up at the same time.  The revised interface and the commentary on the interface will be posted shortly.  By the way, I am posting this entry using the beta version of Live Writer using the Live Writer plug-in for code formatting written by Steve Dunn.  Works quite nicely.

Another class suitable for subclassing.

namespace Interfaces
{
/// <summary>
/// The TOCElement holds the data
/// for a single table of contents entry.
/// This includes the corresponding document index,
/// the title of the TOC entry, and the depth of the
/// TOC entry. The "root" entry is at level zero; no
/// other TOC entry can be at level zero. The Chapters
/// are at level 1, the sections are at level 2,
/// and the subsections are at level 3. Note that the
/// class is immutable; no changes can be made once the
/// class is instantiated.
/// </summary>
public class TOCElement
{
DocumentIndex _DocumentIndex;
string _Title;
int _Level;

public TOCElement(DocumentIndex docIndex,
string title, int level)
{
_DocumentIndex
= docIndex;
_Title
= title;
_Level
= level;
}

public DocumentIndex DocumentIndex
{
get {return _DocumentIndex;}
}
public string Title
{
get {return _Title;}
}
public int Level
{
get {return _Level;}
}
}
}

The DocumentIndex Class

This class is one of several classes that I use in the revised document management interface.  I am including them as separate entries so that you can have multiple web pages up at the same time.  The revised interface will be posted shortly.  By the way, I am posting this entry using the beta version of Live Writer using the Live Writer plug-in for code formatting written by Steve Dunn.  Works quite nicely.

The document index class as shown below is a wrapper around a single string.  Looked at from the point of view of reducing the number of lines of code, this looks a bit silly.  Looked at from the point of view of expressing the intent unambiguously, it is not so silly, especially when a document comes along that needs more than a simple string.

namespace Interfaces
{
/// <summary>
/// This class holds the reference to a particular portion
/// of the document.
/// </summary>
/// <remarks>
/// The intent of this class is that it can be used
/// as is for documents that can use a single string as a pointer
/// into the document or as a base class for a derived child
/// class that may use a more compilicated document indexing
/// scheme. Also, the intent is that a given
/// DocumentIndex instance be immutable once it is
/// instantiated.
/// </remarks>
public class DocumentIndex
{

private string _Index;

public DocumentIndex(string index)
{
_Index
= index;
}

public string Index
{
get {return _Index;}
}
}
}