A Win Form SVN Browser In COBOL





0/5 (0 vote)
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.
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"("" ) 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.
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.