Showing posts with label linux. Show all posts
Showing posts with label linux. Show all posts

Saturday, October 26, 2013

Moving back to Linux on the Laptop


Over the last 4 years I have owned a Macbook Pro. Couple of weeks ago (couple of hours after posting my previous blog post) the hard disk went click-click (I heard it, I had headphones on at the time) and died. After about 8-10 hours of googling and trying out various troubleshooting advices, I was able to resurrect the machine by formatting my hard disk and reinstalling the operating system from the recovery disk partition written to by OSX 10.8 (Mountain Lion), in the process suffering complete data loss.

I learned later that other users who recently upgraded to Mountain Lion had the same experience - disk crash, fixed by reformat and OSX reinstall. I had upgraded because I wanted to move to Java 1.7 - Snow Leopard, the previous OS, had support only for Java 1.6. I was able to recover all of my "production" Python scripts - stuff I wrote long ago but use day-to-day almost without thinking - thanks to a SugarSync account that only partially works (but it worked "well enough" in this particular case).

I was also able to get back most of my recent code (except the work in progress stuff I hadn't pushed) from various code repositories (GitHub, BitBucket, SourceForge, etc). Most of my EBooks are also available for re-download from my accounts at various publishers (Manning, Packt). I lost almost all my music except some songs which were scattered among various devices - EMusic was kind enough to give me a $50 credit to rebuild (a kernel of) my library.

Of course, the Macbook is about 4 years old and its not as if I expected to own it forever. I did hope that I could use it for about 6 years, given that the average life of my PC based laptops are about 2-3 years and this cost almost 2.5 times as much as one of those. At that point, I had planned on going back to using a Linux based PC laptop. Having heard the sound of the disk drive, I was pretty sure it was an unrecoverable hardware failure, so I went ahead and put this plan into motion. A bit premature, as it turned out, but I guess I have to live with the consequences of my actions...

The one major reason I liked the Macbook was its long battery life - 8 hours new, 4-5 hours now, still more than adequate for my purposes. I found one such system at LinuxCertified.com, a store that installs (and guarantees) Linux on popular laptop hardware. Since I have installed Linux before, I just wanted to know what hardware they use so as to maximize my chances of getting all features working under Linux.

I then took the specs for the Lenovo T530, added a few things - upgrade the CPU to a 4 core i5, increased memory from 2GB to 8GB, added an NVIDIA graphics card to support higher resolution (1600x900), and upgraded to the 9 cell (8 hours) battery instead of the 6 cell default - and placed an order at the Lenovo store. Ended up paying about the same amount (with tax) as the one advertised in LinuxCertified.com.

Once the Macbook came back to life, I had a change of heart and tried to cancel the order but Lenovo's return/cancellation policy is pretty draconian (at least by US standards) - once you hit the submit button, you have effectively bought it. Anyway, I figured that the disk on the Macbook crashed once, its only a matter of time before it dies again, so maybe just makes more sense to put it to pasture working on things like my kids' homework :-).

For the OS on the new Thinkpad, I had a 64 bit Ubuntu 13.04 disk from OSDisc.com purchased earlier for my work desktop. The only issue I faced was that the installer could not recognize the wired network during installation - however, once the machine came up with Ubuntu, it was able to recognize the wired (and wireless) connections. I am able to load Google-Chrome beta (to replace the Firefox default browser), watch and listen to lecture videos (for Coursera and other MOOC classes I am taking - I mention it because I can't get sound to work on my desktop). I haven't begun to use it fully (I am still typing this post on my Macbook), but all the software I need works fine on it (so far at least).

Looks wise, of course, this is a huge downgrade - almost like swapping a sports car for an SUV (the real kind) - the Macbook Pro is slim, sleek and sliver while the Thinkpad is thick, chunky and black. With respect to the OS though, I have worked with Unix long enough so Linux design choices just seem more logical, and I have always found the Mac OSX /Application, /Library, /System, etc trees somewhat strange and non-intuitive. Plus there is just more good free stuff on Linux than on OSX, so I am guessing my overall experience with this change would be positive.

So anyway, back to using Linux for everything again :-).

Sunday, February 21, 2010

Debugging PHP with NetBeans on Mac and Linux

As you probably know, I recently started working with Drupal, with lots of help from the Pro Drupal book. In the book (page 524), the author states:

Real-time debugging is a feature of PHP and not Drupal, but it's worth covering, since you can easily be identified as a Drupal ninja if a real-time debugger is running on your laptop.

In my relentless quest for Drupal ninjahood, I naturally wanted to get debugging working with NetBeans, my PHP editor of choice. Just kidding ... the need to get debugging working was driven by the relatively opaque nature of Drupal - the only two debugging features I am aware of are the watchdog logging feature, which I have used, and the Drupal Devel module, which I have not (yet). Also, like most Java programmers, I am probably more dependent on a good IDE than most PHP programmers.

In any case, this post talks about what needs to be done to get PHP debugging working on NetBeans on Linux (CentOS) and Mac (Snow Leopard). The information was gleaned from multiple posts, some of which provided inaccurate or incomplete information, so there was some amount of trial and error involved.

I will assume that you have NetBeans installed and you occasionally use it. I use NetBeans only for scripting, and so far, the lack of a debugger (or my lack of knowledge on how to run it) has not affected me. With Drupal, however, there is no real way of knowing what module(s) are getting called in a request, save from writing watchdog calls in your code, so having a debugger to step through the code can be quite helpful.

This part probably doesn't matter unless you are also doing Drupal development, but there appears to be a Goldilocks syndrome thing going between Drupal 6.15 and PHP 5.2. My CentOS 5.3 has PHP 5.1.6 in the default yum repository. Apparently that doesn't quite cut it with Drupal, so I initially enabled Remi's repository based on information in Binit Bhatia's post - but that now gives me PHP 5.3 (not surprising, since Binit's post is almost a year old now), which has even bigger problems with some of the modules. Ultimately, I ended up getting the 5.2 from the CentOS testing repository following guidelines in Irakli Nadareishvili's post.

On the Mac, I am using MAMP, which has PHP 5.2 installed as a component within MAMP (ie, under /Applications/MAMP/bin/php5/bin), even though at the OS level it has PHP 5.3 installed (ie, under /usr/bin).

So anyway, what you need to do is to install and configure XDebug to work with PHP and NetBeans. On CentOS, just download the source and build it as explained in the XDebug install page.

1
2
3
4
sujit@lysdexic:xdebug$ phpize
sujit@lysdexic:xdebug$ ./configure --enable-xdebug
sujit@lysdexic:xdebug$ make
sujit@lysdexic:xdebug$ sudo cp modules/xdebug.so /usr/lib64/httpd/modules/xdebug.so

On the Mac, compiling from source did not work, neither against the PHP 5.3 in /usr/bin nor the PHP 5.2 packaged with MAMP. It fails on make, and it looks like a bad #ifdef in the code somewhere. However, I was able to get it by downloading the PHP Remote Debugging package from the ActiveState Komodo Site and copying the xdebug.so file under the 5.2 directory to /Applications/MAMP/bin/php5/lib/extensions/no-debug-non-zts-20060613/ - most of this information came from Felix Geisendörfer's post.

The next step is to hook XDebug with PHP and NetBeans. Thankfully, this is (almost) identical on both Mac and CentOS. Essentially, the following lines need to be added to the php.ini file (/etc/php.ini on CentOS and /Applications/MAMP/conf/php5/php.ini on Mac). The information below mostly comes from the Debugging PHP Source Code in the NetBeans IDE NetBeans article, although some of it has been changed using information from other posts.

1
2
3
4
5
6
7
8
[xdebug]
zend_extension=/path/to/xdebug.so
xdebug.remote_enable=1
xdebug.remote_handler=dbgp
xdebug.remote_host=localhost
xdebug.remote_port=9000
xdebug.profiler_enable=1
xdebug.profiler_output=/tmp

In addition, remove/comment any zend_* properties from the php.ini file. Apparently XDebug and Zend don't go well together.

Restart your webserver and bring up a page with phpinfo() and verify that XDebug is available, or use php -m to list the modules as specified in the XDebug install page. If XDebug seems to be installed okay, the next step is to check if NetBeans can see XDebug.

To verify this, start NetBeans, open an existing PHP project (such as a Drupal application), then click the debug button. You should see a "Connecting to XDebug" message on the status bar which should change to "netbeans-xdebug" within a few seconds. At this point, you can debug a request by clicking a URL on your browser and stepping through the code, setting breakpoints, inspecting variables, etc on NetBeans.

Sunday, June 03, 2007

Restoring Windows XP on a Toshiba Satellite running Fedora

Last week I posted an article describing my experiences converting over my new Fujitsu Lifebook over from Microsoft Vista to Ubuntu Linux. This week I talk about going the other way for my old Toshiba Satellite laptop, restoring it back to use Windows XP using the Toshiba OEM Restore disk. I need to do this because the laptop will now be used by my (elementary) school-going son, who uses Windows XP on the family desktop computer and at school, so we don't want him getting confused switching back and forth between Linux and Windows. There are also a lot of games and educational software which run only on Windows and Mac OSX. I suppose I could get him to run these over Wine, but when I last tried it couple of years ago, I could not get sound to work, so I did not even try this time. Finally, according to my wife, one Linux snob is one too many for any given household, and we don't want to argue with that :-).

Just sticking the OEM Restore CD in and restarting the machine should theoretically be all that I needed to do, but if that was all it took, I wouldn't be writing this up. What I got instead, at the end of the restore process, was a machine that, when switched on, came up with an empty black screen, with the word "GRUB" on the left top corner.

Searching on the net, I found instances similar to this situation, where the machine responds with "Grub error 17". The problem was that GRUB (The Grand Unified Boot Loader that most Linux distributions use for booting) is written on the Master Boot Record (or MBR). Windows also needs to boot using information from the MBR. But apparently, the restore process cannot write to the MBR if it is already occupied by GRUB.

The solution is to clean up the MBR somehow. I found a rather vitriolic but quite detailed article from a person who made the switch from Windows to Linux, and was describing his frustrations and switch back after an extremely unhappy few weeks with Linux. The article describes the problems with "Grub error 17", and mentions the Ultimate Boot CD (aka UBCD), which solved the problem.

The Ultimate Boot CD is a free collection of PC system software tools, which can be used to boot up a PC. It reminds me of my system software tool collection (an overflowing case of 5.25" floppy disks) when I worked in software support many years ago, but it is of course much more comprehensive. Its available as an ISO, and I was able to burn a CD-RW from my Ubuntu laptop by downloading the ISO to my Desktop, right clicking the file and select "Write to CD", then "Ok" to actually burn the image to the CD.

I then booted the laptop with the UBCD, selected Filesystem Tools, then Partition Tools, then XFDISK (Extended FDISK). I then removed the one single partition and exited. Restoring from the OEM Restore CD and restarting sent me back to the original GRUB prompt, although it did tell me that it was doing a quick format of the C: drive.

I tried again, rebooting from the UBCD, selecting Filesystem Tools, then Partition Tools, then MBR Tools. A menu comes up, and I selected "Wipe MBR". Restoring from the OEM Restore CD and restarting worked this time. I booted into the original Windows XP operating system that the machine came with.

Anyway, that was all I had this week. While it did not take that long to write, it did take me a while to come to this solution. Having destroyed couple of CD-Rs before on my Fedora system, I was not sure I would be able to burn the UBCD disk right, but ultimately I found an Ubuntu tutorial which described the process. I am also relatively clueless about Windows and DOS, having stopped using both regularly quite some time ago, so it was quite a learning experience. My biggest concern was to not destroy the disk by using a wrong command, so I was extra careful, which takes more time. But I guess things turned out OK at the end.

Saturday, May 26, 2007

Installing Ubuntu on a Fujitsu Lifebook A3120

As I mentioned in last week's post, I purchased a new Fujitsu Lifebook A3120. I've been busy lately, and consequently did not get any time to do any online research, something I did when I purchased my last laptop. I happened to be at Fry's Electronics, something which is quite rare for me lately, and I happened to be in the market for a new laptop, and the machine looked really nice, and I remembered reading somewhere that Fujitsu machines have a good reputation among the Linux community as being fairly easy to convert. And Linux installations have been getting slicker with more and more devices being supported. So it was something of an impulse buy, with a giant leap of faith in Fujitsu and recent Linux distributions. Kind of dumb, now that I think about it, but it mostly worked out OK, at least so far, so I guess alls well that ends well.

As it turns out, the machine got rave reviews for its looks, but not so much for its performance. One thing that disappointed me was its poor battery capacity, about 1.5 hours. Thats what my Toshiba gave me after about 2 years of use, when new the battery life was close to 3 hours. Its not a catastrophe, since I use the batteries during my commute to and from work, which is 50 minutes long. I just have to remember to charge the machine at home and at work. I was hoping they would sell a longer lasting battery, so I could just upgrade at some point, but according to their website, thats the only model they sell. Not sure what the guys at Fujitsu were thinking, but this can't be good for their image. For my part, I feel disappointed and cheated, since I had just assumed that modern laptops are built to provide at least 3 hours of battery life, and upto 6 if you shell out the big bucks. I guess thats the price you pay for not researching before buying.

The Lifebook is a AMD Turion 64 bit dual core machine. It came pre-installed with 32-bit Vista. I have been using Fedora for quite some time now, but a few colleagues suggested using Ubuntu, which is based off the Debian Linux distribution. I have been thinking of switching over to Debian for a while, but my last attempt to switch to Debian was a disaster, so I switched back to Fedora at that time. However, Ubuntu comes packaged as a Live CD, which allows you to run it off the CD, to see if all your devices are recognized, whether X comes up correctly, etc. So I decided to make the switch to Ubuntu.

To ease the transition, I bought and read Marcel Gagne's Moving to Ubuntu Linux, which was very helpful. It also came with a 32-bit Live CD, which worked fine. I tried downloading the 64-bit Ubuntu 6.06 (Dapper Drake), which promised long term support and roasting it with xcdroast on my Fedora box, but after toasting (pun intended) two CD-R's, a colleague burnt me the 64-bit Live CD using Nero.

I installed the 64-bit Dapper Drake Ubuntu from the Live CD, which was surprisingly painless. X worked right out of the box, and everything looked good. I even loved the human theme, with its swirling brown wallpaper, something I am told causes a lot of people extreme revulsion. The one thing that was almost completely unusable was the touchpad. It seemed to be way too sensitive, and would recognize a single tap as a double tap sometimes. At other times, I would have to double-click where I would normally expect to single click. A bit of searching on the Ubuntu forums led me to a variety of suggestions, from installing QSynaptics and configuring the touchpad through it, to replacing the InputDevice section for the touchpad with a custom one.

Turns out I had the Alps Glidepoint, which works with the Synaptics driver, but requires different configuration because the hardware is slower than the Synaptics hardware.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
sujit@sirocco:~$ cat /proc/bus/input/devices 
...
I: Bus=0011 Vendor=0002 Product=0008 Version=7321
N: Name="AlpsPS/2 ALPS GlidePoint"
P: Phys=isa0060/serio2/input0
S: Sysfs=/class/input/input3
H: Handlers=mouse2 ts2 event3 
B: EV=f
B: KEY=420 70000 0 0 0 0
B: REL=3
B: ABS=1000003
...

I installed qsynaptics as the threads asked, but tweaking the parameters did not help much. I then copied the new configuration verbatim from this Ubuntu Forum thread, which actually solved the problem. The following section replaces the section that was called "Synaptics Touchpad" in the default /etc/X11/xorg.conf file. I did notice that the touchpad would go revert back to the old behavior each time I switched from using AC to battery, but I am not noticing it anymore with the distribution upgrade to Feisty Fawn (more on that later).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
Section "InputDevice"
  Identifier  "Synaptics Touchpad"
  Driver    "synaptics"
  Option    "AlwaysCore"
  Option    "SendCoreEvents"  "true"
  Option    "Device"  "/dev/input/event2"
  Option    "Protocol"  "event"
  Option    "LeftEdge"  "130"
  Option    "RightEdge"  "840"
  Option    "TopEdge"  "130"
  Option    "BottomEdge"  "640"
  Option    "FingerLow"  "7"
  Option    "FingerHigh"  "8"
  Option    "MaxTapTime"  "180"
  Option    "MaxTapMove"  "110"
  Option    "ClickTime"  "0"
  Option    "EmulateMidButtonTime"  "75"
  Option    "VertScrollDelta"  "20"
  Option    "HorizScrollDelta"  "20"
  Option    "MinSpeed"  "0.60"
  Option    "MaxSpeed"  "1.10"
  Option    "AccelFactor"  "0.030"
  Option    "EdgeMotionMinSpeed"  "200"
  Option    "EdgeMotionMaxSpeed"  "200"
  Option    "UpDownScrolling"  "1"
  Option    "CircularScrolling"  "1"
  Option    "CircScrollDelta"  "0.1"
  Option    "CircScrollTrigger"  "2"
  Option    "SHMConfig"  "on"
  Option    "Emulate3Buttons"  "on"
EndSection

I then proceeded to install pwsafe, which is a simple command line password manager which I already used. The pwsafe code is available as source, and surprisingly, the C compiler did not come standard with Ubuntu desktop. Not that surprising though, if one thinks about it, since it is built for home users. To build pwsafe, you need to do the configure, make, make install sequence. Configure flagged down the missing components, and I used apt-get install to install them. Once done, I just dropped in the .pwsafe.dat file from my old 32-bit machine and it just worked.

Next I installed Java 6 (from Sun's website). Most modern Linux distributions include Java 1.4 which you can turn off by setting the JAVA_HOME variable to the current one in your .bash_profile.

I then installed the latest versions of Maven (my favorite Java build tool), and Ant (the build tool in use at work), and Eclipse 3.2.2, my favorite IDE. This time, I opted to put all the optional software under /opt, to avoid confusion and keep things clean. These need extra environment variables, which I also set in my .bash_profile.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# Java settings
export JAVA_HOME=/opt/jdk1.6.0_01
export PATH=$JAVA_HOME/bin:$PATH
# Ant settings
export ANT_HOME=/opt/apache-ant-1.7.0
export PATH=$ANT_HOME/bin:$PATH
# Maven settings
export MAVEN_HOME=/opt/maven-2.0.6
export PATH=$MAVEN_HOME/bin:$PATH
# CVS Access (Work, sourceforge)
export CVS_RSH=ssh
#export CVSROOT=...
export CVSROOT=...

I also had to add this line to the end of /etc/profile for my .bash_profile to be actually called each time I logged on.

1
. $HOME/.bash_profile

The next step was to figure out how to connect to my employer's VPN. For my old Fedora laptop, I used PPTP Client for my current and previous employer. This time around, I was unable to install the PPTPConfig component, a GUI setup tool, since some of the libraries that PPTPConfig depends on are not available in the debian repositories as 64-bit packages. I saw some threads where they suggest either installing the 32-bit packages and the associated 32-bit library packages, or compiling from source, but I was not familiar enough with Debian/Ubuntu to do this. There were also a few threads that suggested using the Network Manager applet that was available in Feisty Fawn (Ubuntu 7.04) in place of the PPTPConfig program.

So I had two options. Either upgrade to 7.04 to get the Network Manager, or figure out how to do the configuration manually. I was actually able to connect to the VPN with the manual configuration, but none of the machines in the network behind the VPN were visible. This meant that I had to change my route to make these machines visible, but I know almost nothing about routes. I did notice that Dapper Drake seemed quite old (I got Apache 1.3 from the Drake repository for example, whereas I have been using Apache 2 for at least a year now on my old Fedora laptop), and since one of the main reasons I chose Debian/Ubuntu was because of its ease of upgrades, I decided to go with upgrading to Feisty Fawn.

Doing a distribution upgrade is actually quite easy. All you do is invoke the update manager, and a GUI will tell you if a new distribution is available, and lead you through the steps. Unfortunately, it will let you upgrade one release at a time. It took me about 4 hours to upgrade from Dapper (6.06) to Edgy (6.10), and another 3 hours to upgrade from Edgy (6.10) to Feisty (7.04).

Distribution upgrades using the update manager (apt dist-upgrade) are also apparently fraught with danger, based on the comments on the forums, and most people prefer doing a full install. I found out why, since X started crapping out with strange "command not found" errors when I upgraded to Edgy. The problem was that bash just got stricter, and the fix was to change the /bin/sh at the top of /etc/gdm/Xsession to /bin/bash. I found this quite easily from the forums however. There were no issues with upgrading from Edgy to Feisty.

With Feisty, there is a GUI which allows you to configure your VPNs, see available wireless hotspots, and restart your wired connection. Its pretty neat and very convenient, but sadly, I was not able to connect to my employer's VPN using it. This was the error message I saw in /var/log/messages when I tried to connect. If anyone knows why, or if there is a fix or workaround, please let me know. I have also posted a question in the forums, may post it as a bug if its not already there.

1
2
May 26 14:41:00 localhost kernel: [ 1659.772358] nm-ppp-auth-dia[10538]: segfault at 
0000000000000088 rip 00002abc6a6d221b rsp 00007fff45f70720 error 4

So I had to revert back to the manual method. Luckily, this time (after half a day's search), I found the magic route commands that will allow me to see and ssh to the machines behind the VPN in this thread.

I ended up with a solution that was quite a hack. I mean, pppd is so nicely structured to call the scripts /etc/ppp/ip-up, which will call /etc/ppp/ip-up.local and/or scripts in the /etc/ppp/ip-up.d/ directory when the VPN link is being established, and the corresponding scripts /etc/ppp/ip-down when being torn down. I could not make any of these work. Ultimately, as described in the thread, it just turned out to be a shell script that calls pon and poff and adds the route commands at the end of pon. However, whats important is that it works, and I can wait for the real fix to NetworkManager when it becomes available. Here is what my script looks like:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#!/bin/bash
case $1 in
  'on')
    echo "Starting VPN tunnel to company..."
    sudo pon vpn_company
    sleep 10
    sudo route add -net 10.1.128.0 netmask 255.255.255.0 dev ppp0
    sudo route add -net 10.1.1.0 netmask 255.255.255.0 dev ppp0
  'off')
    echo "Stopping VPN tunnel to company..."
    sudo poff vpn_company
    ;;
esac

And my manually configured tunnel file (/etc/ppp/peers/vpn_company) looks like this. It is mostly copied from the file generated by PPTPConfig on my Fedora laptop. The other files required for the configuration are listed in the "Configuring a tunnel by hand" section in the PPTPClient documentation.

1
2
3
4
5
6
7
8
pty "pptp vpn.mycompany.com --nolaunchpppd "
name MYCOMPANY\\sujit
remotename PPTP
require-mppe
file /etc/ppp/options.pptp
ipparam vpn_mycompany
usepeerdns
noauth

There are still a few other things I want to have working. I do have a wireless access point at home, but I have to get my wife off her PC to be able to configure it, and its set up to do MAC address authentication at the moment, so I was not able to test the wireless. However, the laptop is able to see the access point, so I think wireless should already work. This was quite refreshing compared to Fedora 2, where I had to compile the madwifi drivers for the integrated Atheros wireless card into the kernel. I never got around to doing this after upgrading to Fedora 3, as a result of which I haven't used wireless for quite a long while.

Thankfully, the laptop is at a point where its usable. It has all the software I need, the touchpad works, and I can connect through the VPN. Major thanks are due to the good folks who actually post to the Ubuntu Forums, one of the most comprehensive and active forums I have seen lately. There will definitely be more things I find and want as I explore Ubuntu and new softwares that are available here, and if I have issues with installing or using them, I may write about it here in the future.

Friday, July 21, 2006

Python XML Viewer for Linux

I recently needed to view an XML file I had generated, to verify that the code worked right. Normally I would just view the file with Firefox using the file:// protocol, and Firefox would show me the document tree with its default XML rendering. But this file was quite large (about 800MB) and Firefox was not able to complete loading the file. It did not crash, but the keyboard and mouse became unresponsive and I had to manually kill Firefox from the command prompt.

So I figured that since Firefox is a general purpose browser, it was probably expecting too much to ask it to render such large files, and I would have better luck with software that was optimized to only render XML - in other words, an XML viewer. So I did a quick search for "XMLViewer" on Google, but came up empty in terms of software I could actually use. There appears to be many more XML Viewers for Windows world than for Linux. The only ones I came up with were KXMLViewer and gxmlviewer.

The screenshots for KXMLViewer look nice but the functionality was not exactly what I wanted. Gxmlviewer was tested on RedHat 7.1 and has probably not been updated since. I was unable to either install the RPM or build from source using the downloads on Fedora Core 3. I could probably have done it if I had tried a little harder, but I decided to pass.

Having failed to find tools to do what I wanted, I started wondering if I probably should just build it myself. Since I wanted to parse large XML files, using a SAX parser seemed to be the obvious choice. As a matter of fact, one of the reasons Firefox was crashing because it was trying to slurp in all the 800MB of data before trying to render it. It needs to do that because it allows you additional controls to expand and collapse elements. That functionality would be a nice-to-have for me, but I definitely did not need it. All I wanted was something that would format the XML (which was written for compactness) into something that I could read. Writing a SAX parser that formats the output into a nice indented document tree structures is practically one of the first examples you encounter when you read about SAX parsing. So building such a tool would be trivial.

To navigate, ie go up or down one or more pages can be done simply using the Linux utility less. The [SPACE] key moves the formatted output forward a page, and [CTRL-B] moves it back. To call the tool, specify the file name and the tabstop indent.

1
$ xmlcat.py my_really_large_xml_file.xml 4 | less

I wrote xmlcat.py in Python. Here is the code.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
#!/usr/bin/python
# A simple SAX Parser to view large XML files as a nicely formatted XML
# document tree. Pipe the output through less and move forward and backward
# using [SPACE] and [CTRL-B] respectively. Standard less keyboard commands
# will also work.
#
import string
import sys
from xml.sax import make_parser
from xml.sax.handler import ContentHandler

class PrettyPrintingContentHandler(ContentHandler):
    """ Subclass of the SAX ContentHandler to print document tree """

    def __init__(self, indent):
        """ Ctor """
        self.indent = indent
        self.level = 0
        self.chars = ''

    def startElement(self, name, attrs):
        """ Set the level and print opening tag with attributes """
        self.level = self.level + 1
        attrString = ""
        qnames = attrs.getQNames()
        for i in range(0, len(qnames)):
            attrString = attrString + " " + qnames[i] + "=\"" + attrs.getValueByQName(qnames[i]) + "\""
        print self.tab(self.level) + "<" + string.rstrip(name) + attrString + ">"

    def endElement(self, name):
        """ Print the characters and the closing tag """
        if (len(string.strip(self.chars)) > 0):
            print self.tab(self.level + 1) + string.rstrip(self.chars)
        self.chars = ''
        print self.tab(self.level) + "</" + string.rstrip(name) + ">"
        self.level = self.level - 1

    def characters(self, c):
        """ Accumulate characters, ignore whitespace """
        if (len(string.strip(c)) > 0):
            self.chars = self.chars + c

    def tab(self, n):
        """ Print the tabstop for the current element """
        tab = ""
        for i in range(1, n):
            for j in range(1, int(self.indent)):
                tab = tab + " "
        return tab

def usage():
    """ Print the usage """
    print "Usage: xmlcat.py xml_file indent | less"
    print "Use [SPACE] and [CTRL-B] to move forward and backward"
    sys.exit(-1)

def main():
    """ Check the arguments, instantiate the parser and parse """
    if (len(sys.argv) != 3):
        usage()
    file = sys.argv[1]
    indent = sys.argv[2]
    parser = make_parser()
    prettyPrintingContentHandler = PrettyPrintingContentHandler(indent)
    parser.setContentHandler(prettyPrintingContentHandler)
    parser.parse(file)

if __name__ == "__main__":
    main()

As you can see, the code is quite trivial and based in large part on this DevShed article, but I am including it here anyway. It took me all of half an hour to write and test, so its not rocket science, but it may help you save half an hour when you are looking for a similar tool, and you stumble upon this page.

Saturday, April 29, 2006

Setting up a C#/.NET Dev Environment with Mono

I have been interested in learning C# and applying it to a real application for a while. Since I work in a Java/J2EE shop, the closest I got were doing some toy example apps in my spare time. However, an opportunity came up recently to integrate a prebuilt C#/ASP.NET application into our site, and I had a chance to help out with it. This article describes a small C# application with ADO.NET to connect to a MySQL database and ASP.NET for the web pages that I set up and developed with MonoDevelop on Linux using Mono. I did this to refresh my understanding of how to use these components to stitch together a useful .NET application. Because my basic worldview is that of a Java programmer, the article draws parallels with Java frameworks. I am hoping that this would be of use to Java programmers who are looking to venture out into C# and .NET applications using Mono on Linux.

C#/.NET code should be quite easy to pick up for Java/J2EE developers since the concepts are quite similar, although the terminology is different. Overall, I think .NET provides a tighter stack with fewer moving parts, because they take an intrinsic "convention over configuration" approach with their components. I guess it's easier for Microsoft to do this because they provide all the components in the stack, unlike the plug-and-play approach taken by Java developers. But on to the terminology differences - here are some I found:

C#/.NET Term Java/J2EE Term Comment
CLR (Common Language Runtime) JVM (Java Virtual Machine) CLR targets multiple languages on a single platform (now multiple with Mono running on Linux, Windows and Mac OSX), and the JVM targeting one language on multiple platforms (although that may be changing too, with the scripting support JSR).
ASP.NET JSP This is only partially true, because ASP.NET has more functionality built in. The closest analogy on the Java side I can think of is Tapestry. Like Tapestry with its .java, .page and .html files, ASP.NET has the .aspx.cs and .aspx files. The .aspx.cs file is a C# subclass of Page which contains logic and references to we controls such as buttons, text boxes, etc. These controls and all methods in the .aspx.cs file can be accessed from the .aspx file.
ADO.NET JDBC Again, only partially true. The JDBC abstraction, ie the ability to write the same code regardless of the target database, which Java programs all know and love, is available in the form of an OLE database driver in .NET but not widely used because of speed issues. The standard practice in ADO.NET is to use drivers with native support for different databases, so your code will be different if you were using the ByteFX drivers for MySQL versus the Microsoft SqlClient driver for MS-SQL.
Assembly JAR/WAR The deployable unit for a .NET application is a .DLL or an .EXE, depending on whether they are libraries which will be included as references into another project, or standalone executable applications.
GAC (Global Assembly Cache) Your Maven2 repository, if you use one Some people use Maven, so this is roughly equivalent to the Maven2 repository. Redhat also includes a Java RPM with JARs for the Apache commons projects and some others.
mcs javac The C# compiler from Mono.
mono java -jar Allows you to run "executable" JAR files (the ones with the main method specified in the META-INF/MANIFEST.MF file).

There are other one-to-one mappings, such as monodoc for javadoc. For building .NET apps from the command line, you can use NAnt, which is Ant for .NET. Nunit, the unit testing framework for .NET is based on JUnit, but has much richer annotation support than the commonly used JUnit 3.8. JUnit 4.0, which was in beta last I looked, looks a lot like Nunit. I haven't used NAnt or Nunit myself, though.

As for the language itself, C# is also very similar to Java, although conventions are different, such as uppercasing the first character of all method calls. There is also a much greater reliance on operator overloading (using == instead of Java's .equals() for equality checks and map["key"] instead of Java's map.get(key), for example). There are also other little things such as the "using" keyword, which is kind of like the "static import" in Java 1.5, and "namespace" instead of "package". The get/set notation in C# is also quite cool, saves a lot of typing.

But enough generalities. Let me tell you what I did, which was the objective of this article anyway. I set out to develop a simple database backed web application with 2-3 ASPX pages. My database consisted of a single MySQL table of book reviews, with the following (non-normalized for simplicity) schema.

1
2
3
4
5
6
7
8
9
mysql> describe books;
+-------------+--------------+------+-----+---------+----------------+
| Field       | Type         | Null | Key | Default | Extra          |
+-------------+--------------+------+-----+---------+----------------+
| id          | bigint(20)   |      | PRI | NULL    | auto_increment |
| name        | varchar(255) |      |     |         |                |
| author      | varchar(255) |      |     |         |                |
| review      | text         |      |     |         |                |
+-------------+--------------+------+-----+---------+----------------+

My first step was to create a Console application in MonoDevelop to wrap the table with a DAO (Data Access Object). The DAO will provide methods to add rows to the table, return all rows, and find reviews based on reviewer. Note that creating an empty application in MonoDevelop is not a good idea, because then the Assembly.cs file will not be created. The Assembly.cs file is similar to the META-INF/MANIFEST.MF file, and is required. Here is the DAO code.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
// BookReviewDao.cs
using System;
using System.Data;
 
using ByteFX.Data.MySqlClient;
 
namespace BookReviews {
 
    /// <summary>
    /// This class interfaces with the MySQL database containing
    /// the book review data.
    /// </summary>
    public class BookReviewDao {
 
        string dbHost = "localhost";
        string dbName = "bookshelfdb";
        string dbUser = "root";
        string dbPass = "mysql";
         
        MySqlConnection conn;
 
        // Construct an instance of the BookReviewDao object.
        public BookReviewDao() {
            string connectionString = String.Format("Persist Security Info=False;Server={0};Database={1};User Id={2};Password={3}", dbHost, dbName, dbUser, dbPass);
            Console.Out.WriteLine(connectionString);
            conn = new MySqlConnection(connectionString);
        }
         
        // Returns all book review records from the database.
        public DataSet FindAll() {
            DataSet ds = new DataSet("FindAll");
            try {
                conn.Open();
                MySqlDataAdapter adapter = new MySqlDataAdapter("select name, author, review from books", conn);
                adapter.Fill(ds);
                conn.Close();
            } catch (Exception e) {
                Console.Error.WriteLine(e);
            } finally {
                if (conn.State == ConnectionState.Open) {
                    conn.Close();
                }
            }
            return ds;
        }
         
        // Returns all book review records by the specified author.
        public DataSet FindByUser(string authorName) {
            DataSet ds = new DataSet("FindByUser");
            try {
                String selectQuery = String.Format("select name, author, review from books where author='{0}'", authorName);
                conn.Open();
                MySqlDataAdapter adapter = new MySqlDataAdapter(selectQuery, conn);
                adapter.Fill(ds);
                conn.Close();
            } catch (Exception e) {
                Console.Error.WriteLine(e);
            } finally {
                if (conn.State == ConnectionState.Open) {
                    conn.Close();
                }
            }
            return ds;
        }
         
        // Inserts a single book review record into the database.
        public void AddData(String bookName, String authorName, String review) {            try {
                MySqlCommand command = new MySqlCommand("insert into books(name, author, review) values (@name, @author, @review)", conn);
                conn.Open();
                command.Parameters.Add("@name", bookName);
                command.Parameters.Add("@author", authorName);
                command.Parameters.Add("@review", review);
                command.ExecuteNonQuery();
                conn.Close();
            } catch (Exception e) {
                Console.Error.WriteLine(e);
            } finally {
                if (conn.State == ConnectionState.Open) {
                    conn.Close();
                }
            }
        }
 
        // Returns true if user is already in the system, else returns false.
        public bool IsValidUser(String authorName) {
            Int64 numRows = 0;
            try {
                MySqlCommand command = new MySqlCommand(String.Format("select count(*) from books where author='{0}'", authorName), conn);
                conn.Open();
                numRows = (Int64) command.ExecuteScalar(); // bug in ByteFX, returns Int64 instead of Int8 (int)
                conn.Close();
            } catch (Exception e) {
                Console.Error.WriteLine(e);
            } finally {
                if (conn.State == ConnectionState.Open) {
                    conn.Close();
                }
            }
            return (numRows > 0);
        }
    }
     
}

One thing to note is that ByteFX works with the authentication system for MySQL 4.0, so if you are using a higher version, you will need to downgrade its authentication to the "old-password" mode. Here is how this is done, I found this in a newsgroup posting on the web.

1
2
3
mysql> use mysql;
mysql> set password for root@localhost=old_password('mysql');
mysql> flush privileges;

Since my application is still a console application, I could call the MainClass.Main method to add my records and test the functioning of the other methods. Here is the MainClass.cs code.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
// Main.cs
using System;
using System.Data;
 
namespace BookReviews {
 
    class MainClass {
     
        private void Find(string authorName) {
            BookReviewDao dao = new BookReviewDao();
            DataSet ds = dao.FindByUser(authorName);
            Format(ds);
        }
         
        private void FindAll() {
            BookReviewDao dao = new BookReviewDao();
            DataSet ds = dao.FindAll();
            Format(ds);
        }
         
        private void Format(DataSet ds) {
            DataTable books = ds.Tables[0]; // bug in ByteFX, return "Table" for TableName.
            for (int curCol = 0; curCol < books.Columns.Count; curCol++) {
                Console.Out.Write(books.Columns[curCol].ColumnName.Trim() + "\t");
            }
            Console.Out.WriteLine();
            for (int curRow = 0; curRow < books.Rows.Count; curRow++) {
                for (int curCol = 0; curCol < books.Columns.Count; curCol++) {
                    Console.Out.Write(books.Rows[curRow][curCol].ToString().Trim() + "\t");
                }
                Console.Out.WriteLine();
            }
        }
                 
        private void Add() {
            for (;;) {
                Console.Out.Write("Book name:");
                string bookName = Console.In.ReadLine();
                Console.Out.Write("Author:");
                string authorName = Console.In.ReadLine();
                Console.Out.Write("Review:");
                string review = Console.In.ReadLine();
                BookReviewDao dao = new BookReviewDao();
                dao.AddData(bookName, authorName, review);
                Console.Out.Write("Add another (y/n)[y]?");
                string yorn = Console.In.ReadLine();
                if (yorn.StartsWith("n")) {
                    break;
                } else {
                    continue;
                }
            }
        }
         
        private bool IsValid(string authorName) {
            BookReviewDao dao = new BookReviewDao();
            return dao.IsValidUser(authorName);
        }
         
        private void Help() {
            Console.Out.WriteLine("Welcome to BookReviews");
            Console.Out.WriteLine("Valid options are:");
            Console.Out.WriteLine("--find: find by author");
            Console.Out.WriteLine("--findall: find all records");
            Console.Out.WriteLine("--add: add new record");
            Console.Out.WriteLine("--help: display this message");
        }
         
        public static void Main(string[] args) {
            MainClass bookReviews = new MainClass();
            if (args.Length == 0) {
                bookReviews.Help();
            } else {
                if (args[0] == "--find") {
                    string authorName = args[1];
                    bookReviews.Find(authorName);
                } else if (args[0] == "--findall") {
                    bookReviews.FindAll();
                } else if (args[0] == "--add") {
                    bookReviews.Add();
                } else if (args[0] == "--valid") {
                    string authorName = args[1];
                    bookReviews.IsValid(authorName);
                } else {
                    bookReviews.Help();
                }
            }
        }
    }
}

Using this, I populated the table with some test data. Here is how it was called.

1
2
$ cd bin/Debug
$ mono BookReviews.exe --add

Since I really want the DAO to be used by my ASP.NET web application, I converted this to be a library. You can do this by right clicking on project, selecting Configurations, change Compile_Target from Executable to Library. Code generated in bin/Debug/*.exe changes to bin/Debug/*.dll.

The next step is to create the ASPX pages and the corresponding ASPX.CS (called Code-behind) C# class code. I visualized a mini-app with a login page which shows a list of reviews made by you. This page has a link to all reviews. The all reviews page has a link back to the review by the particular logged in user. So I needed a Index.aspx page for the login page and the List.aspx page for the list pages. Mono has no template for a Web Project (unlike the Microsoft Visual Studio IDE on Windows). I copied some boilerplate code from Charlie Calvert's page for Global.asax.cs and Index.aspx.cs and List.aspx.cs, and added the required event handlers to the last two files. Here is the code for the .aspx pages and the corresponding .aspx.cs pages.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
<!-- Index.aspx -->
<%@ Page language="c#" Codebehind="Index.aspx.cs" AutoEventWireup="false" Inherits="BookReviews.Index" %>
<Html>
    <Head>
        <Title>BookReviews Login</Title>
    </Head>
    <Body>
        <center><asp:Label id="lblMessage" runat="server" text="Please login" /></center>
        <form id="loginForm" method="post" runat="server">
            <asp:Label id="lblUsername" style="Z-INDEX: 101; LEFT: 144px; POSITION: absolute; TOP: 164px" runat="server" text="Username:" />
            <asp:TextBox id="txtUsername" style="Z-INDEX: 101; LEFT: 256px; POSITION: absolute; TOP: 164px" runat="server" />
            <asp:Label id="lblPassword" style="Z-INDEX: 101; LEFT: 144px; POSITION: absolute; TOP: 228px" runat="server" text="Password:" />
            <asp:TextBox id="txtPassword" style="Z-INDEX: 101; LEFT: 256px; POSITION: absolute; TOP: 228px" runat="server" />
            <asp:Button id="btnLogin" style="Z-INDEX: 101; LEFT: 144px; POSITION: absolute; TOP: 292px" runat="server" text="Login" />
            <asp:Button id="btnCancel" style="Z-INDEX: 101; LEFT: 256px; POSITION: absolute; TOP: 292px" runat="server" text="Cancel" />
        </form>     </Body>
</Html>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
// Index.aspx.cs
using System;
using System.Drawing;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
 
namespace BookReviews {
 
    // <summary>
    // Code-behind component for the Index.aspx page. Defines event
    // handlers for this page.
    // </summary>
    public class Index : Page {
     
        protected Label lblUsername;
        protected Label lblPassword;
        protected TextBox txtUsername;
        protected TextBox txtPassword;
        protected Button btnLogin;
        protected Button btnCancel;
        protected Label lblMessage;
     
        // generated code, if mono could do this. This is copied from this site:        // http://bdn.borland.com/article/0,1410,32057,00.html
         
        private void Page_Load(object sender, System.EventArgs e) {
            // user code to initialize the page
        }
         
        protected override void OnInit(System.EventArgs e) {
            InitializeComponent();
            base.OnInit(e);
        }
         
        protected void InitializeComponent() {
            this.btnCancel.Click += new System.EventHandler(this.BtnCancel_Click);
            this.btnLogin.Click += new System.EventHandler(this.BtnLogin_Click);            this.Load += new System.EventHandler(this.Page_Load);
        }
         
        // end of generated code
 
        // <summary>
        // Acts just like a reset, except that this is done in C#
        // code, on response to a click event on this cancel button.
        // </summary>
        private void BtnCancel_Click(object sender, System.EventArgs e) {
            txtUsername.Text = "";
            txtPassword.Text = "";
        }
 
        // <summary>
        // Fired in response to a click event on the login button. Sends
        // off to the BookReviewDao for authentication, and redirects to
        // the list page if successful, else, displays an error and re-prompts
        // for user name and password. If login is successful, then it
        // also puts the user name in the session. We later look it up as
        // needed for any personalization in the other pages.
        // </summary>
        private void BtnLogin_Click(object sender, System.EventArgs e) {
            BookReviewDao dao = new BookReviewDao();
            if (dao.IsValidUser(txtUsername.Text)) {
                // we dont do much password validation, all
                // we care about is that he is already in our
                // system
                Session["userName"] = txtUsername.Text;
                Response.Redirect("/List.aspx?author=" + txtUsername.Text);
            } else {
                lblMessage.Text = "Invalid Login, please try again";
                lblMessage.ForeColor = Color.Red;
            }
        }
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
<!-- List.aspx -->
<%@ Page language="c#" Codebehind="List.aspx.cs" AutoEventWireup="false" Inherits="BookReviews.List" %>
<Html>
    <Head>
        <Title>BookReviews List</Title>
    </Head>
    <Body>
        <form runat="server">
            <asp:DataList runat="server" id="bookReviewDl">
                <HeaderTemplate>List of book reviews</HeaderTemplate>
                <ItemTemplate>
                    <%# DataBinder.Eval(Container.DataItem, "name") %></td>
                    <td><%# DataBinder.Eval(Container.DataItem, "author") %></td>
                    <td><%# DataBinder.Eval(Container.DataItem, "review") %>
                </ItemTemplate>
                <FooterTemplate />
            </asp:DataList>
            <a href="<%= GetOtherListHyperLink().NavigateUrl %>"><%= GetOtherListHyperLink().Text %></a>
        </form>
    </Body>
</Html>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
// List.aspx.cs
using System;
using System.Collections;
using System.Collections.Specialized;
using System.Data;
using System.Drawing;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
 
namespace BookReviews {
 
    // <summary>
    // Code-behind component of the List.aspx page.
    // </summary>
    public class List : Page {
     
        protected HyperLink lnkOtherList;
        protected DataList bookReviewDl;
     
        // generated code, if mono could do this. This is copied from this site:        // http://bdn.borland.com/article/0,1410,32057,00.html
         
        public void Page_Load(object sender, System.EventArgs e) {
            // user code to initialize the page
            if (!IsPostBack) {
                // scan the parameters looking for author
                NameValueCollection parameters = Request.Params;
                ICollection keys = (ICollection) parameters.Keys;
                string author = null;
                foreach (string key in keys) {
                    if (key == "author") {
                        author = (string) parameters["author"];
                        break;
                    }
                }
                BookReviewDao dao = new BookReviewDao();
                DataSet reviewDs;
                if (author == null) {
                    reviewDs = dao.FindAll();
                    lnkOtherList = new HyperLink();
                    lnkOtherList.Text = "Show book reviews by " + ((string) Session["userName"]);
                    lnkOtherList.NavigateUrl = "/List.aspx?author=" + ((string) Session["userName"]);
                } else {
                    reviewDs = dao.FindByUser(author);
                    lnkOtherList = new HyperLink();
                    lnkOtherList.Text = "Show all book reviews";
                    lnkOtherList.NavigateUrl = "/List.aspx";
                }
                bookReviewDl.DataSource = createDataSource(reviewDs);
                bookReviewDl.DataBind();
            }
        }
         
        protected override void OnInit(System.EventArgs e) {
            InitializeComponent();
            base.OnInit(e);
        }
         
        protected void InitializeComponent() {
            this.Load += new System.EventHandler(this.Page_Load);
        }
         
        // end of generated code
 
        // <summary>
        // Creates a in-memory Collection that can be used by the aspx
        // page based on the DataSet object returned from the Dao.
        // </summary>
        public ICollection createDataSource(DataSet ds) {
            DataTable books = ds.Tables[0]; // bug in ByteFX, return "Table" for TableName.
            bool debug = true;
            if (debug) {
                for (int curCol = 0; curCol < books.Columns.Count; curCol++) {
                    Console.Out.Write(books.Columns[curCol].ColumnName.Trim() + "\t");
                }
                Console.Out.WriteLine();
                for (int curRow = 0; curRow < books.Rows.Count; curRow++) {
                    for (int curCol = 0; curCol < books.Columns.Count; curCol++) {
                        Console.Out.Write(books.Rows[curRow][curCol].ToString().Trim() + "\t");
                    }
                    Console.Out.WriteLine();
                }
            }
            return new DataView(books);
        }
 
        // <summary>
        // Returns a reference to the HyperLink control.
        // </summary>
        public HyperLink GetOtherListHyperLink() {
            return lnkOtherList;
        }
    }
}

Basically, the Page.Page_Load method is called when the page is loaded and the *_Click events are triggered from the mouse clicks on the page. Not very hard to understand if you've already been on the Tapestry learning curve, but may require a bit of unlearning for classic J2EE programmers.

The Web.config file I used is minimal, copied from the demo web.config file from the xsp sample directory.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
<?xml version="1.0" encoding="utf-8"?>
<!-- Web.config -->
<configuration>
 
    <system.web>
      <customErrors mode="Off"/>
 
      <authentication mode= "None">
      </authentication>
 
      <httpModules>
      </httpModules>
 
    </system.web>
 
    <appSettings>
      <add key="MonoServerDefaultIndexFiles" value="index.aspx, Default.aspx, default.aspx, index.html, index.htm" />
   </appSettings>
 
</configuration>

To run the pages, I used the xsp server, which is distributed as part of Mono. You can also add the mod_mono to the Apache httpd webserver to serve ASP pages in a production environment. To do this, make sure that the .DLL file generated by MonoDevelop when you build your project is in the bin/ directory of your project. It is usually in bin/Debug, but I would just move it before starting xsp. xsp should be started in your project directory.

1
2
3
4
5
6
7
8
$ pwd
/home/sujit/Projects/BookReviews
$ ls
AssemblyInfo.cs   BookReviews.mdp   Global.asax.cs  List.aspx     Web.config
bin               BookReviews.mds   Index.aspx      List.aspx.cs
BookReviewDao.cs  BookReviews.pidb  Index.aspx.cs   Main.cs
$ mv bin/Debug/BookReviews.dll bin/
$ xsp

The application is now browseable at http://localhost/Index.aspx. Since I was running on Linux, I used Mozilla Firefox. I mention this only because there is a general belief that ASP pages cannot be used from browsers other than MS Internet Explorer. This may be true if you were using client side Visual Basic scripts instead of Javascript, but since ASPX is all done server-side, pages can be served to any regular browser.

My final step was to import the project I was going to integrate into MonoDevelop on my Linux desktop. MonoDevelop has a "Import Visual Studio project" option. The important thing to note is that you will need to select the .csproj file, which is where Visual Studio keeps its project information. If the imported project consists of multiple subprojects, as mine did, each of them needs to be imported separately as solutions. The dependencies will be need to be set up manually by compiling the low level modules separately into DLLs, and then adding them as references to the main solution.

Here are some resources I found useful. There are a lot of resources on the internet dealing with .NET development, some of it served by Microsoft (MSDN), and some from the Code Project.

I also found the following books useful. Of course, I had bought these some time back, hoping to use them one day, but never did. So arguably, I used these since these were the ones I had lying around, but they are worth looking at if you want to start out with C#/.NET development.

  • Mono - A Developer's Notebook by Edd Dumbill and Niel M Bornstein - A great book if you want to get up and running with C# and .NET using Mono really quickly. It lacks depth though, so be prepared to do a little work looking up details on the web.
  • C# and the .NET Platform by Andrew Troelsen - The link is to the second edition. I have the first (beta) edition, and admittedly, there are some places where its dated (the code does not work). But it provides a very detailed introduction to the various components that make up the .NET framework.

Compared to Visual Studio, which I also installed on my Windows computer, MonoDevelop is faster to get up and running. But Visual Studio is much more slicker, although as an Eclipse user, I think it could benefit from some of the features available there.

I think Mono would be very useful as a simple (simpler than C/C++) language to build interfaces to system tools on Linux. Apparently the Mono developers seem to think so too, as evidenced by their greater support for Gtk# and Glade# project templates. RedHat seems to have standardized on Python for this work, but Novell is making some steps in that direction with SuSE.