Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C++

A Win Form SVN Browser In COBOL

0.00/5 (No votes)
23 Dec 2009CC (ASA 2.5)4 min read 8.8K  
Introduction - Why? this project sprang from a real need. Whilst it is possible to browse subversion from a web browser, the handling of non html files is a pain. We wanted a program which would show html files as web pages and all other files as text.

Introduction - Why?

this project sprang from a real need. Whilst it is possible to browse subversion from a web browser, the handling of non html files is a pain. We wanted a program which would show html files as web pages and all other files as text. This approach alows people to just click around the svn tree seeing what is there and looking side README files etc.

the reason for doing this is COBOL is simple - why not? Out of the big three .net business languages, COBOL handles this sort of thing just as well as VB and C#. Not only that, but good examples of using COBOL for .net in to interact with the rest of the CLR and class libraries are hard to come by.

Click on images to see full size in new window

What?

This project is build around two major pieces. There is the main form and the HttpGetter. The http getter ( which started life as an RSS reader) uses System.Net.HttpWebRequest to send GET requests to the web server interface of the svn installation. These are managed via MainForm which displays the directory structure reported from the svn server in a tree view in the left hand pane and when a node in the view is clicked it shows a view of the contents of that node in a web browser control in the right hand pane (see image top right).

If a node in the tree is a directory, this is indicated by a trailing /. To avoid having to download the entire tree structure up front, the tree is not populated with the child nodes of a directory until that directory node is click upon. When a directory which has been populated is clicked upon a second time the node is expanded. This means double clicking on a directory node populates it and expands it all at once.

When the application is run, it will ask for the server and login details for the svn server. The code will handle the url with or without the http:// prefix. If the svn server does not require a username and password then they do not have to be supplied. In the examples I have used here, I have connected to the publically avalible shellinabox.googlecode.com/svn/trunk server. I have put in a user name and password to illustrate the screen shot - but actually the server ignores these!

How?

All this application (100% of the code) is written in Micro Focus COBOL for .net. The example here is based on Studio Enterprise Edition 6.0. However, the code should work with no source code alterations in the free achademic version of the net-express product.

Here we can see the code being stepped through in Visual Studio 2008. Yes - this really is COBOL! It is amazing how far the language has come from its humble 1950's begining.

For me, the most interesting parts are the interaction with the http protocol and parsing the html which comes back from the svn web server. However, the web form stuff may be of interest as well. On the http side, we can see on the code in the source below, however I would like to highlight the following:

perform varying httpKey thru response::"Headers"::"Keys"
    set header to String::"Format"("{0}: {1}" httpKey response::"Headers"::"Item"(httpKey))
  If httpKey::"ToLower" equals "content-encoding" Then
      set contentEncoding to response::"Headers"::"Item"(httpKey)
  End-If
end-perform

This is a nice example of iterating through a collection, we get the content encoding from the response keys in a completely civilised way! By so doing we are then able to handle compressed streams using standard .net classes:

set rs to response::"GetResponseStream"
If contentEncoding not equals null Then
  If contentEncoding::"ToLower" equals "gzip" Then
      set rs to New "System.IO.Compression.GZipStream"(
          rs type "System.IO.Compression.CompressionMode"::"Decompress")
  Else
    If contentEncoding::"ToLower" equals "deflate" Then
        set rs to New "System.IO.Compression.DeflateStream"(
             rs type "System.IO.Compression.CompressionMode"::"Decompress")
    End-If
  End-If
End-If

Next we can look at the html parser. This is really simple because the svn web server puts out the structure of the svn tree using html lists with each list element on a new line. However, it does work out as a really nice demonstration of using CLR generics inside COBOL. We use the String::Split method to get an array of lines and then take the bits we want from them at append them to a "System.Collections.Generic.List"[string]. We can see here that the generic type is String, which in COBOL is set using the [] syntax. By using a list, we avoid all the trouble of having to know how many elements there might be up front. Working in COBOL for .net really is nothing like as hard as working in classic COBOL!

       method-id ParseHtml.
       local-storage section.
           01 rawLines string occurs any.
           01 rawLine  string.
           01 chars    character occurs any.
           01 rets     type "System.Collections.Generic.List"[string].
           01 blocks   string occurs any.
       procedure division using by value htmlToParse as string
           returning urls as string occurs any.
           set content of chars to (x'0A' as character)
           set rawLines to htmlToParse::"Split"(x'0A' as character) 
           set rets to new "System.Collections.Generic.List"[string].
           perform varying rawLine through rawLines
               if rawLine::"Trim"::"ToLower"::"StartsWith"("

<li>") Then
                   set blocks to rawLine::"Split"('"' as character)
                   invoke rets::"Add"(blocks(2))
               end-if
           end-perform
           set urls to rets::"ToArray"
       end method ParseHtml. 
</li>

The Source

HTTPGetter.cbl

$set sourceformat(variable).
 class-id. HTTPGetter as "COBOLSVNBrowser.HTTPGetter".

 object.
 working-storage section.

 method-id DoRSSRequest.
 local-storage section.
     01 request   type "System.Net.HttpWebRequest".
     01 encoder   type "System.Text.ASCIIEncoding".
     01 response  type "System.Net.WebResponse".
     01 httpKey   string.
     01 exp       type "System.InvalidOperationException".
     01 rs        type "System.IO.Stream".
     01 respSt    type "System.IO.StreamReader".
     01 contentEncoding string.
     01 header    string.
 procedure division  using by value
     url      as string
     username as string
     password as string
     returning retHtml as string.

     set request to type "System.Net.WebRequest"::"Create"(url)
         as type "System.Net.HttpWebRequest"
     set request::"Method" to "GET"
     set request::"ContentType" to "application/x-www-form-urlencoded"
     set request::"Credentials" to new "System.Net.NetworkCredential"(username password)
     invoke request::"Headers"::"Add"("Accept-Encoding" "gzip,deflate")

     set request::"ProtocolVersion" to type "System.Net.HttpVersion"::"Version11"
     set request::"KeepAlive" to false
     set request::"ServicePoint"::"Expect100Continue" to false

     *> Get results
     perform varying httpKey thru request::"Headers"::"Keys"
             set header to String::"Format"("{0}: {1}" httpKey request::"Headers"::"Item"(httpKey))
     end-perform

     Try
         set response to request::"GetResponse"
     Catch exp
         *> Debug only - should handle with a form really!
         display "RSS Request Failed:"
         perform varying httpKey thru request::"Headers"::"Keys"
                 set header to String::"Format"("{0}: {1}" httpKey request::"Headers"::"Item"(httpKey))
         end-perform

         *> try again
         set response to request::"GetResponse"
     End-Try

     perform varying httpKey thru response::"Headers"::"Keys"
         set header to String::"Format"("{0}: {1}" httpKey response::"Headers"::"Item"(
             httpKey))
       If httpKey::"ToLower" equals "content-encoding" Then
           set contentEncoding to response::"Headers"::"Item"(httpKey)
       End-If
     end-perform

     set rs to response::"GetResponseStream"
     If contentEncoding not equals null Then
       If contentEncoding::"ToLower" equals "gzip" Then
           set rs to New "System.IO.Compression.GZipStream"(rs type
              "System.IO.Compression.CompressionMode"::"Decompress")
       Else
         If contentEncoding::"ToLower" equals "deflate" Then
             set rs to New "System.IO.Compression.DeflateStream"(rs type
                 "System.IO.Compression.CompressionMode"::"Decompress")
         End-If
       End-If
     End-If

     set encoder to type "System.Text.ASCIIEncoding"::"New"
     set respSt to type "System.IO.StreamReader"::"New"(rs encoder)
     set retHtml to respSt::"ReadToEnd"
     invoke respSt::"Close"
 end method DoRSSRequest.


 method-id ParseHtml.
 local-storage section.
     01 rawLines string occurs any.
     01 rawLine  string.
     01 chars    character occurs any.
     01 rets     type "System.Collections.Generic.List"[string].
     01 blocks   string occurs any.
 procedure division using by value htmlToParse as string
     returning urls as string occurs any.
     set content of chars to (x'0A' as character)
     set rawLines to htmlToParse::"Split"(x'0A' as character)
     set rets to new "System.Collections.Generic.List"[string].
     perform varying rawLine through rawLines
         if rawLine::"Trim"::"ToLower"::"StartsWith"("<li>") Then
             set blocks to rawLine::"Split"('"' as character)
             invoke rets::"Add"(blocks(2))
         end-if
     end-perform
     set urls to rets::"ToArray"
 end method ParseHtml.

 end object.
 end class HTTPGetter.

LoginForm.cbl

 *> TODO: Insert code to perform custom authentication using the provided username and password
 *> The custom principal can then be attached to the current thread's principal as follows:
 *>     My.User.CurrentPrincipal = CustomPrincipal
 *> where CustomPrincipal is the IPrincipal implementation used to perform authentication.
 *> Subsequently, My.User will return identity information encapsulated in the CustomPrincipal object
 *> such as the username, display name, etc.

  class-id. LoginForm1 as "COBOLSVNBrowser.LoginForm1" is partial
inherits type "System.Windows.Forms.Form".

  environment division.
  configuration section.
  repository.

  object.
  working-storage section.
      01 repo string public.
      01 username string public.
      01 password string public.

  method-id. NEW.
  procedure division.
      invoke self::"InitializeComponent"
      goback.
  end method NEW.

  method-id.  "btnOK_Click" final private.
  procedure division using by value sender as object e as type "System.EventArgs".
      invoke self::"AllDone"
      goback
  end method "btnOK_Click".

  method-id.  "btnCancel_Click" final private.
  procedure division using by value sender as object e as type "System.EventArgs".
      set self::"repo" to null
      set self::"username" to null
      set self::"password" to null
      invoke self::"Close"
      goback
  end method "btnCancel_Click".

  method-id.  "LoginForm1_KeyPress" final private.
  procedure division using by value sender as object e as type "System.Windows.Forms.KeyPressEventArgs".
      if e::"KeyChar" equals 13 then
          invoke self::"AllDone"
      end-if
      goback
  end method "LoginForm1_KeyPress".

  method-id. "AllDone" final private.
      set self::"repo" to self::"tbRepo"::"Text"
      set self::"username" to self::"tbUserName"::"Text"
      set self::"password" to self::"tbPassword"::"Text"
      invoke self::"Close"
      goback
  end method "AllDone".

  end object.
  end class LoginForm1.

LoginForm1.Designer.cbl

  class-id. LoginForm1 as "COBOLSVNBrowser.LoginForm1" is partial
inherits type "System.Windows.Forms.Form".
  environment division.
  configuration section.
  repository.

  object.
  working-storage section.
  01 label1 type "System.Windows.Forms.Label".
  01 label2 type "System.Windows.Forms.Label".
  01 btnOK type "System.Windows.Forms.Button".
  01 btnCancel type "System.Windows.Forms.Button".
  01 tbUserName type "System.Windows.Forms.TextBox".
  01 tbPassword type "System.Windows.Forms.TextBox".
  01 label3 type "System.Windows.Forms.Label".
  01 tbRepo type "System.Windows.Forms.TextBox".
  01 components type "System.ComponentModel.IContainer".

 *> Required method for Designer support - do not modify
 *> the contents of this method with the code editor.
  method-id.  "InitializeComponent" private.
  procedure division.
  set btnOK to new "System.Windows.Forms.Button"
  set btnCancel to new "System.Windows.Forms.Button"
  set label1 to new "System.Windows.Forms.Label"
  set label2 to new "System.Windows.Forms.Label"
  set tbUserName to new "System.Windows.Forms.TextBox"
  set tbPassword to new "System.Windows.Forms.TextBox"
  set tbRepo to new "System.Windows.Forms.TextBox"
  set label3 to new "System.Windows.Forms.Label"
  invoke self::"SuspendLayout"
 *>
 *> btnOK
 *>
  set btnOK::"Location" to new "System.Drawing.Point"( 12 165)
  set btnOK::"Name" to "btnOK"
  set btnOK::"Size" to new "System.Drawing.Size"( 75 23)
  set btnOK::"TabIndex" to 4
  set btnOK::"Text" to "OK"
  set btnOK::"UseVisualStyleBackColor" to True
  invoke btnOK::"add_Click"(new "System.EventHandler"(self::"btnOK_Click"))
 *>
 *> btnCancel
 *>
  set btnCancel::"Location" to new "System.Drawing.Point"( 147 165)
  set btnCancel::"Name" to "btnCancel"
  set btnCancel::"Size" to new "System.Drawing.Size"( 75 23)
  set btnCancel::"TabIndex" to 5
  set btnCancel::"Text" to "Cancel"
  set btnCancel::"UseVisualStyleBackColor" to True
  invoke btnCancel::"add_Click"(new "System.EventHandler"(self::"btnCancel_Click"))
 *>
 *> label1
 *>
  set label1::"AutoSize" to True
  set label1::"Location" to new "System.Drawing.Point"( 12 56)
  set label1::"Name" to "label1"
  set label1::"Size" to new "System.Drawing.Size"( 58 13)
  set label1::"TabIndex" to 0
  set label1::"Text" to "&User name"
 *>
 *> label2
 *>
  set label2::"AutoSize" to True
  set label2::"Location" to new "System.Drawing.Point"( 12 110)
  set label2::"Name" to "label2"
  set label2::"Size" to new "System.Drawing.Size"( 53 13)
  set label2::"TabIndex" to 0
  set label2::"Text" to "&Password"
 *>
 *> tbUserName
 *>
  set tbUserName::"Location" to new "System.Drawing.Point"( 12 75)
  set tbUserName::"Name" to "tbUserName"
  set tbUserName::"Size" to new "System.Drawing.Size"( 210 20)
  set tbUserName::"TabIndex" to 1
  invoke tbUserName::"add_KeyPress"(
      new "System.Windows.Forms.KeyPressEventHandler"(self::"LoginForm1_KeyPress"))
 *>
 *> tbPassword
 *>
  set tbPassword::"Location" to new "System.Drawing.Point"( 12 126)
  set tbPassword::"Name" to "tbPassword"
  set tbPassword::"PasswordChar" to '*'
  set tbPassword::"Size" to new "System.Drawing.Size"( 210 20)
  set tbPassword::"TabIndex" to 3
  invoke tbPassword::"add_KeyPress"(
     new "System.Windows.Forms.KeyPressEventHandler"(self::"LoginForm1_KeyPress"))
 *>
 *> tbRepo
 *>
  set tbRepo::"Location" to new "System.Drawing.Point"( 13 23)
  set tbRepo::"Name" to "tbRepo"
  set tbRepo::"Size" to new "System.Drawing.Size"( 210 20)
  set tbRepo::"TabIndex" to 1
 *>
 *> label3
 *>
  set label3::"AutoSize" to True
  set label3::"Location" to new "System.Drawing.Point"( 13 7)
  set label3::"Name" to "label3"
  set label3::"Size" to new "System.Drawing.Size"( 57 13)
  set label3::"TabIndex" to 0
  set label3::"Text" to "&Repoistory"
 *>
 *> LoginForm1
 *>
  set self::"ClientSize" to new "System.Drawing.Size"( 236 201)
  invoke self::"Controls"::"Add"(tbRepo)
  invoke self::"Controls"::"Add"(label3)
  invoke self::"Controls"::"Add"(tbPassword)
  invoke self::"Controls"::"Add"(tbUserName)
  invoke self::"Controls"::"Add"(label2)
  invoke self::"Controls"::"Add"(label1)
  invoke self::"Controls"::"Add"(btnCancel)
  invoke self::"Controls"::"Add"(btnOK)
  set self::"Name" to "LoginForm1"
  set self::"Text" to "SVN Login"
  invoke self::"add_KeyPress"(new "System.Windows.Forms.KeyPressEventHandler"(self::"LoginForm1_KeyPress"))
  invoke self::"ResumeLayout"(False)
  invoke self::"PerformLayout"
  end method "InitializeComponent".

 *> Clean up any resources being used.
  method-id. "Dispose" override protected.
  procedure division using by value disposing as condition-value.
      if disposing then
        if components not = null then
          invoke components::"Dispose"()
        end-if
      end-if
      invoke super::"Dispose"(by value disposing)
      goback.
  end method "Dispose".

  end object.
  end class LoginForm1.

Main.cbl

class-id. Main as "COBOLSVNBrowser.Main".
environment division.
configuration section.
repository.

static.

method-id. Main
    custom-attribute is type "System.STAThreadAttribute".
local-storage section.
01 mainForm type "COBOLSVNBrowser.MainForm".
procedure division.

    set mainForm to new "COBOLSVNBrowser.MainForm"()
    invoke type "System.Windows.Forms.Application"::"Run"(mainForm)
    goback.

end method "Main".

end static.
end class Main.

MainForm.html

$set sourceformat(variable).
 class-id. MainForm as "COBOLSVNBrowser.MainForm" is partial
           inherits type "System.Windows.Forms.Form".

 object.
 working-storage section.
     01 root     string.
     01 userName string.
     01 password string.

 method-id. NEW.
 local-storage section.
     01 node type "System.Windows.Forms.TreeNode".
     01 login  type "COBOLSVNBrowser.LoginForm1".
 procedure division.
     invoke self::"InitializeComponent"()
     set login to new type "COBOLSVNBrowser.LoginForm1"
     invoke login::"ShowDialog"
     if login::"username" equals null then
         goback
     end-if
        move login::"repo" to root
     if not root::"ToLower"::"StartsWith"("http://") then
        move String::"Format"("http://{0}" login::"repo") to root
     end-if
     if not root::"ToLower"::"EndsWith"("/") then
        move String::"Format"("{0}/" root) to root
     end-if
     move login::"username" to userName
     move login::"password" to passWord
     set node to new "System.Windows.Forms.TreeNode"("root")
     invoke self::"treeView1"::"Nodes"::"Add"(node)
     invoke self::"AddToTree"(self::"GetUrls"(root) node)
     goback.
 end method NEW.

 method-id GetUrls.
     01 getter type "COBOLSVNBrowser.HTTPGetter".
     01 txt    string.
 procedure division using by value url as string returning urls as string occurs any.
     set getter to new "COBOLSVNBrowser.HTTPGetter"()
     set txt To getter::"DoRSSRequest"(url userName password)
     set size of urls to 0
     if url::"ToLower"::"EndsWith"("html") or url::"ToLower"::"EndsWith"("htm") then
         set self::"MainViewerBrowser"::"DocumentText" To txt
     else
         if url::"EndsWith"("/") then
             set self::"MainViewerBrowser"::"DocumentText" To String::"Format"(
                 "<html><body><h2>Directory:</h2>{0}</body></html>" url)
             set urls to getter::"ParseHtml"(txt)
         else
             set self::"MainViewerBrowser"::"DocumentText"  To String::"Format"(
                "<html><body><pre>{0}</pre></body></html>" txt::"Replace"("<" "<"))
         end-if
     end-if
     goback.
 end method GetUrls.

 method-id. AddToTree.
 local-storage section.
     01 tnode type "System.Windows.Forms.TreeNode".
     01 url string.
 procedure division using urls as string occurs any node as type "System.Windows.Forms.TreeNode".
     perform varying url through urls
         if url not equals("../") then
             invoke node::"Nodes"::"Add"(new "System.Windows.Forms.TreeNode"(url))
         end-if
     end-perform
 end method AddToTree.

 method-id.  "treeView1_AfterSelect" final private.
 local-storage section.
     01 url string value "".
     01 node type "System.Windows.Forms.TreeNode".
     01 ex type "System.Exception".
 procedure division using by value sender as object e as type "System.Windows.Forms.TreeViewEventArgs".
     set node to e::"Node"
     perform until exit
         if node::"Parent" equals null then
             exit perform
         end-if
         set url to string::"Concat"(node::"Text" url)
         move node::"Parent" to node
     end-perform
     set url to string::"Concat"(root url)
     invoke self::"AddToTree"(self::"GetUrls"(url) e::"Node")
 end method "treeView1_AfterSelect".

 end object.
 end class MainForm.

MainForm.Designer.cbl

 class-id. MainForm as "COBOLSVNBrowser.MainForm" is partial
           inherits type "System.Windows.Forms.Form".

 environment division.
 configuration section.
 repository.

 object.
 working-storage section.
 01 splitContainer1 type "System.Windows.Forms.SplitContainer".
 01 treeView1 type "System.Windows.Forms.TreeView".
 01 MainViewerBrowser type "System.Windows.Forms.WebBrowser".
 01 components type "System.ComponentModel.IContainer".

*> Required method for Designer support - do not modify
*> the contents of this method with the code editor.
 method-id.  "InitializeComponent" private.
 procedure division.
 set splitContainer1 to new "System.Windows.Forms.SplitContainer"
 set treeView1 to new "System.Windows.Forms.TreeView"
 set MainViewerBrowser to new "System.Windows.Forms.WebBrowser"
 invoke splitContainer1::"Panel1"::"SuspendLayout"
 invoke splitContainer1::"Panel2"::"SuspendLayout"
 invoke splitContainer1::"SuspendLayout"
 invoke self::"SuspendLayout"
*>
*> splitContainer1
*>
 set splitContainer1::"Dock" to type "System.Windows.Forms.DockStyle"::"Fill"
 set splitContainer1::"Location" to new "System.Drawing.Point"( 0 0)
 set splitContainer1::"Name" to "splitContainer1"
*>
*> splitContainer1.Panel1
*>
 invoke splitContainer1::"Panel1"::"Controls"::"Add"(treeView1)
*>
*> splitContainer1.Panel2
*>
 invoke splitContainer1::"Panel2"::"Controls"::"Add"(MainViewerBrowser)
 set splitContainer1::"Size" to new "System.Drawing.Size"( 800 364)
 set splitContainer1::"SplitterDistance" to 266
 set splitContainer1::"TabIndex" to 0
*>
*> treeView1
*>
 set treeView1::"Anchor" to type "System.Windows.Forms.AnchorStyles"::"Top" b-
          or type "System.Windows.Forms.AnchorStyles"::"Bottom" b-
          or type "System.Windows.Forms.AnchorStyles"::"Left" b-or type
     "System.Windows.Forms.AnchorStyles"::"Right" as type "System.Windows.Forms.AnchorStyles"
 set treeView1::"Location" to new "System.Drawing.Point"( 0 0)
 set treeView1::"Name" to "treeView1"
 set treeView1::"Size" to new "System.Drawing.Size"( 263 364)
 set treeView1::"TabIndex" to 0
 invoke treeView1::"add_AfterSelect"(new
     "System.Windows.Forms.TreeViewEventHandler"(self::"treeView1_AfterSelect"))
*>
*> MainViewerBrowser
*>
 set MainViewerBrowser::"Anchor" to type
          "System.Windows.Forms.AnchorStyles"::"Top" b-or type
           "System.Windows.Forms.AnchorStyles"::"Bottom" b-or type
           "System.Windows.Forms.AnchorStyles"::"Left" b-or type
     "System.Windows.Forms.AnchorStyles"::"Right" as type "System.Windows.Forms.AnchorStyles"
 set MainViewerBrowser::"Location" to new "System.Drawing.Point"( 3 0)
 set MainViewerBrowser::"MinimumSize" to new "System.Drawing.Size"( 20 20)
 set MainViewerBrowser::"Name" to "MainViewerBrowser"
 set MainViewerBrowser::"Size" to new "System.Drawing.Size"( 527 364)
 set MainViewerBrowser::"TabIndex" to 0
*>
*> MainForm
*>
 set self::"ClientSize" to new "System.Drawing.Size"( 800 364)
 invoke self::"Controls"::"Add"(splitContainer1)
 set self::"Name" to "MainForm"
 set self::"Text" to "COBOLSVNBrowser"
 invoke splitContainer1::"Panel1"::"ResumeLayout"(False)
 invoke splitContainer1::"Panel2"::"ResumeLayout"(False)
 invoke splitContainer1::"ResumeLayout"(False)
 invoke self::"ResumeLayout"(False)
 end method "InitializeComponent".
*> Clean up any resources being used.
 method-id. "Dispose" override protected.
 procedure division using by value disposing as condition-value.
     if disposing then
       if components not = null then
         invoke components::"Dispose"()
       end-if
     end-if
     invoke super::"Dispose"(by value disposing)
     goback.
 end method "Dispose".

 end object.
 end class MainForm.

License

This article, along with any associated source code and files, is licensed under The Creative Commons Attribution-ShareAlike 2.5 License