Mar 312013
 

I’ve known about tries for sometime. They’re a pretty neat data-structure for storing and looking-up strings.I decided to try and implement one in Java so that I can learn more about them. I’ll post another article later that goes into some more detail about this implementation, but for now you can check out the source here. It’s not production-ready by any means; it’s just me playing around.

Jul 142012
 
Inspired by The Codeless Code:

The master was meditating when a priest respectfully entered his chamber. The master opened his eyes. The priest bowed respectfully and said, “Master, I would like you to look at the code of a young disciple of mine”. The master nodded and followed the priest to a computer. On the screen, was a code listing. The priest pointed to the code and said:

“My disciple created an abstract class and another class that extends the abstract class. However, he has a method that should be of use to all future derivations of the abstract class.”

The master furrowed his brow and looked at the code. “Indeed.”, he said. The priest continued, “I pointed this fact out to him and mentioned that it would be better to define it in the abstract class”. “I see; what did he say?”, asked the master.

The priest sighed and said, “He said ‘You Ain’t Gonna Need It’ and that he could simply copy the method into each new derivation when the time comes”. The master then asked the priest to bring the disciple to him at once. The priest bowed and went away to fetch the young disciple.

A few minutes later, the young disciple respectfully entered the master’s chamber. “You sent for me, master?”, he said. “Yes. I have a task for you”, said the master. The disciple bowed, indicating that he was ready to perform whatever task the master required. The master looked at the disciple and said: “Tomorrow, we will have monks visiting from a neighboring monastery. In their honor, our monastery is providing a feast for them. I need you to report to the dining hall tomorrow. The cook will give you further instructions”. The disciple bowed and left the master’s chamber.

The next morning the disciple arrived at the dining hall as he was asked. He looked around and noticed a large number of seats. He assumed, correctly, that these seats were for the visiting monks. He then noticed the monastery’s cook approaching him. The cook was holding a bowl that was filled with a white substance. As the cook got closer, the disciple realized that it was salt. Once the cook reached the disciple, he held out the bowl to him and said, “Master has asked you to give salt to any of the monks who desire it”. The disciple took the bowl and the cook left. The disciple was puzzled, but smiled thinking that this was going to be an easy task. After all, how many monks would require more salt in their food? He estimated only a few; not much more than that.

In a few minutes, the visiting monks arrived and sat at their places. Other monks from the hosting monastery brought out steaming bowls of soup for the visitors. The head visiting-monk took a spoonful of soup, sipped it and wrinkled his nose. “This soup does not have any salt!” he said, rather loudly. The disciple quickly ran up to the head visiting-monk and bowed and said “Master, I apologize that there is not enough salt in your soup, please allow me to offer you some!” The head monk nodded and the disciple quickly added salt until the monk motioned him to stop. He had barely finished when he heard another monk complaining that there was no salt in his soup either, then another, and another. Soon he was running around the hall at full speed, bringing salt to each of the visiting monks, trying his best to make sure that everyone was happy. The hall was big, and the number of visiting monks was many. When he was done, he sat down exhausted, in the corner of the hall.

He closed his eyes and tried to catch his breath. When he opened his eyes, he noticed that the next course was being brought in. “Surely the cook wouldn’t have forgotten to add salt this time!”, he thought. Unfortunately, it was not so! “There is no salt in my meal!”, thundered the head visiting-monk. The disciple got up and ran to the head monk to add salt his meal. Soon other monks started complaining and the disciple was running around the hall as he had done before, offering salt to all the visiting monks. The disciple hoped that this would be the last time he would have to do this, but alas, there were three more courses! The rest of the courses passed by in a blur for the disciple. All he could remember was that he was running around the entire hall, bringing salt to the visiting monks. The monks didn’t all finish their meals at the same time and so the later courses were continuously being brought out. Hence, he was always on his feet and didn’t get a chance to rest.

Finally, the monks stopped asking for salt and the disciple wearily went back to his corner. Dessert would be next, and there would be no need for salt then. He had barely sat down when he saw the cook approaching him. This time he had another bowl, also filled with a white substance. When the cook got close, he offered the bowl to the disciple and said a single word: “Sugar”. The disciple was almost in tears. He knew was was coming and prepared himself for the endless rushing around that he would have to do. Luckily there was only one course of dessert. When the feast was done, the disciple collapsed in the corner. He opened his eyes and noticed the cook walking towards him again. “What more will he want me to do?”, thought the disciple frantically. He noticed with some relief however, that the cook’s hands were empty. “Master will see you now”, said the cook as he got closer to the disciple. The disciple wearily got up to his feet and walked to the master’s chambers.

After a few minutes, the disciple arrived at the master’s chambers. He walked in and bowed in front of the master, his legs burning with fatigue. “How was your task?” asked the master with his eyes still closed. “It was exceedingly difficult master! There was no salt in the soup or the meals, and no sugar in the desserts! I had to run around the whole hall bringing salt and sugar to the visiting monks!”, said the disciple. “It must have been exceedingly tiring…”, said the master. “Yes, master! It was!”, said the disciple nodding his head. The master opened his eyes and said, “One could say that your task would have been much easier had the salt and sugar been added to the meals at the source, and thus before they were brought out to our honored visitors.”

In that moment, the disciple was enlightened.

Dec 042011
 

I was trying to get pinch-zoom and panning working on an Android view today. Basically I was trying to implement the same behavior you see when you use Google Maps (for example). You can zoom in and pan around until the edge of the image, but no further. Also, if the image is fully zoomed out, you can’t pan the image. Implementing the pinch-zoom functionality was pretty easy. I found an example on StackOverflow. I then wanted to implement panning (or dragging) as well. However, I wasn’t able to easily find examples and tutorials for this functionality. I started with this example that comes from the third edition of the Hello, Android! book but I didn’t get too far. So I started playing around a little bit with the events and started writing some code from scratch (using the example from Hello, Android!) so that I could have a better idea of what was happening.

As I mentioned before, getting zoom to work was pretty easy. Implementing panning/dragging was the hard part. The major issues I encountered and subsequently fixed were the following:

  1. Panning continues indefinitely in all directions.
  2. When you zoom and then pan, stop, and then start again, the view jerks to a new position instead of panning from the existing position.
  3. Excessive panning towards the left and top can be constrained, but panning towards the right and bottom is not so easily constrained.

Once I fixed all the problems, I figured that it would be nice to document it for future reference, and I also think it would be a useful resource for others who have the same problem. Now a little disclaimer before I go any further: I’m not an Android expert and I’m really not that great with graphics; I just started it learning to program for Android this semester for one of my Masters electives. So there might be a better way of doing all this, and if there is, please let me know! Also, if you want to skip all the explanations and just see the code, you can skip to the last page.

Continue reading »

Nov 072011
 

I was using a jQuery plugin called a ajaxfileupload to upload a file through AJAX. Technically what the plugin does isn’t AJAX. It creates a hidden form and an iframe, and then submits the form using the iframe as the target. The iframe will then end up with the response from the server. This response is then read-in by the plugin and handled appropriately. In my case I was using a controller action that would return JSON (using the .action extension). The action uses Spring’s MappingJacksonJSONView that returns JSON with a content type of application/json (as it should). This works perfectly in Chrome, however in both Firefox and IE, the user is presented with a dialog box that asks them to download the JSON response. This is obviously not what I wanted. The reason this is happening is because the response is being directly submitted to the iframe (and therefore, the page). That is, it’s not coming through via the XMLHttpRequest object. So IE and FF don’t know what to do with it and assume that it is something the user would want to download. The solution to this problem is to set the content-type to text/plain. This wasn’t as straightforward as I thought it would be.

Initially I was going to call the render(…) method of MappingJacksonJsonView but that didn’t work because the content-type had already been set to application/json. The solution I came up with was to duplicate some of the code (ugh) inside MappingJacksonJsonView to get the JSON as a string and to then write that to the response:


@RequestMapping
public void processFileUpload(HttpServletResponse response, Model model, ...) {

    ...

    //Set the content-type and charset of the response
    response.setContentType("text/plain");
    response.setCharacterEncoding("UTF-8");

    //I need to use another OutputStream here; I cannot use the response's OutputStream because that will cause errors
    //later on when the JSP needs to render its content (recall that getOutputStream() can only be called exactly once
    //on a response). Therefore I'm writing the data to a ByteArrayOutputStream and then writing the byte array from
    //the ByteArrayOutputStream to the response manually.

    ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
    ObjectMapper objectMapper = new ObjectMapper();
    JsonGenerator generator = objectMapper.getJsonFactory().createJsonGenerator(byteArrayOutputStream, JsonEncoding.UTF8);

    //Before I can convert the data into JSON, I will need to filter some attributes out of the model (namely BindingResult)
    Map<String, Object> result = new HashMap<String, Object>();

    for(Map.Entry<String, Object> entry : model.asMap().entrySet()) {
        if(!(entry.getValue() instanceof BindingResult)) {
            result.put(entry.getKey(), entry.getValue());
        }
    }

    objectMapper.writeValue(generator, result);
    response.getWriter().write(new String(byteArrayOutputStream.toByteArray(), "UTF8"));
}

This still seems a little hacky to me. A possible improvement is to annotate the action with @ResponseBody and return the JSON as a string without involving the response at all. If anyone has a better solution, I’m all ears!

Jul 012011
 

In Spring 3, it’s very easy to get a view to return JSON. However, there is no built-in mechanism to return JSONP. I was able to find a pretty good tutorial That uses Spring’s DelegatingFilterProxy. I implemented this solution and I got it to work, but I didn’t like the fact that I had to create a separate filter and a bunch of other classes just for that. I also wanted to use a specific extension (.jsonp) for JSONP just to make it more explicit. Spring uses MappingJacksonJsonView to return JSON. I figured that I could extend this view and have it return JSONP instead of JSON.
Continue reading »

Nov 222009
 

I finally got around to adding pages to my blog. I’ve added an “About Me” page and a “Projects” page. The “Projects” page is similar to the one I had on the old site. I’ve migrated one project (Sulekha) over. Will be moving the second one (FXCalendar) over soon and I’m also going to try and add another one to the list.

Mar 292009
 

A while ago, I wrote up a quick guide about running X/Windows applications (specifically, aterm) without root windows on Windows, using Cygwin. Recently I tried to set it up again and I realized that some of the information is slightly out of date. I’m also endeavoring to write a better guide. I’m assuming that you have, at the very least, a decent understanding of building things from source. The process under Cygwin is pretty much the same as under any other *nix, but there are a few quirks. On the whole, it’s a whole lot easier than it used to be. This guide is primarily geared towards running aterm with a transparent background on a windows machine so that you can have a decent client for the Cygwin commandline, instead of the crappy Windows one.

I’m assuming that you already have Cygwin installed. If you don’t, you can get it from here. In addition to whatever other packages you have selected to customize your install, you also need development packages (gcc and friends), Xorg packages (headers, includes, and libraries), and a few graphics libraries (for aterm):

  • Devel
    • gcc-core
    • gcc-g++
    • libXaw3d-devel (for xv)
    • libjpeg-devel (for aterm)
    • libpng12-devel (for aterm)
  • Libs
    • jpeg (for aterm)
    • libXaw3d-devel
    • libXaw3d-7
    • libfreetype6
    • libjpeg-devel
    • libjpeg62 (for aterm)
    • libjpeg6b (for aterm)
    • libpng12 (for aterm)
    • libpng12-devel (for aterm)
    • libtiff5 (for aterm, xv)
    • zlib-devel (for aterm)
    • zlib0 (for aterm)
  • Utils
    • bzip2 (to handle .bz2 files)
  • X11
    • libX11-devel
    • xinit
    • xsetroot (if xv doesn’t work for you)

After Cygwin finishes installing those packages, grab the sources for libAfterImage, aterm, and xv. Unpack the sources perform the requisite steps to build and install from source (./configure, make, and make install should work if all goes well).

libAfterImage:

If you get “parse error before XErrorEvent” errors while building libAfterImage, make sure that you didn’t forget to select the X11 development package.

aterm:

gcc on Cygwin expects –rdynamic and not -rdynamic. If you’re seeing these errors, edit the Makefiles under src and src/graphics within the aterm source directory. Change the “-rdynamic” to “–rdynamic”. The changes should be on line 54 for both files.

xv:

Under the tiff directory within the xv sources, there is a file called RANLIB.csh. Edit this file and make sure that you ONLY have the following line in there:

ranlib $1 >& /dev/null

Otherwise the build process will fail. Additionally, you need to edit xv.h. This file lives right at the root of your xv source directory. If you do not perform the following change, you’ll get errors from gcc complaining that “sys_errlist has previously been defined”. Change line 119 of xv.h to:

/*extern char *sys_errlist[]; */    /* this too... */

What you’re doing is commenting out the definition for sys_errlist so that it doesn’t conflict with what has already been defined in the Cygwin header files. These changes should be the only ones you need to get xv compiling and running.

Now you need to set up two batch files. One to start up X rootlessly, and another to start up aterm. Before you do that, make sure you add C:\cygwin\usr\bin and C:\cygwin\X11R6\usr\bin to your PATH variable. You can do this by going to My Computer > Properties > Advanced > Environment Variables. If you don’t do this, you’ll get “cygwin1.dll not found” errors while trying to run these batch files. The X windows binaries used to live in C:\cygwin\usr\X11R6\bin, but have since been moved to C:\cygwin\usr\bin. Therefore, the start-up batch-file now looks like this:

xwin.bat:

C:\cygwin\usr\X11R6\bin\run.exe C:\cygwin\usr\bin\xwin.exe -multiwindow -clipboard -silent-dup-error
C:\cygwin\usr\X11R6\bin\run.exe C:\cygwin\usr\local\bin\xv.exe -display :0 -root -quit -be -max /cygdrive/c/Documents\ and\ Settings/vivin/My\ Documents/My\ Pictures/Wallpapers/01707_spectrumofthesky_1920x1200.jpg

The first line starts up the X windowing system. The second line sets the wallpaper using aterm. You now need another batch file to run aterm, and that looks like this:

aterm.bat

C:\cygwin\usr\X11R6\bin\run.exe C:\cygwin\bin\bash.exe --login -i -c "aterm -sh 80 -tr -trsb -fade 20 -tint gray -sb -st -sr -sl 1000 -tn xterm"

This file starts aterm with the background image at 50% brightness, transparent background, transparent scrollbar, 20% fading on losing focus, gray tint, scrollbar, trough-less scrollbar, scrollbar on the right, 1000 scrollback lines, and with xterm terminal emulation. Like I mentioned in my original guide. xv will sometimes fail to start with xwin. If that is the case, you can modify aterm.bat to look like this:

aterm.bat:

C:\cygwin\usr\X11R6\bin\run.exe C:\cygwin\bin\bash.exe --login -i -c "xv -display :0 -root -quit -be -max /cygdrive/c/Documents\ and\ Settings/vivin/My\ Documents/My\ Pictures/Wallpapers/01707_spectrumofthesky_1920x1200.jpg &amp;amp;amp;amp;&amp;amp;amp;amp; aterm -sh 80 -tr -trsb -fade 20 -tint gray -sb -st -sr -sl 1000 -tn xterm"

Slightly inefficient, but it works. Now if you have a dual-monitor display, you’ll notice that the background image is stretched across both screens when you run aterm. This is probably not what you want. To fix this problem you need to change a few invocation options for xv. For this to work properly (meaning, not look crappy) both screens should be running at the same resolution:

xv -display :0 -root -quit -be -maxpect -rmode 1 /cygdrive/c/Documents\ and\ Settings/vivin/My\ Documents/My\ Pictures/Wallpapers/01707_spectrumofthesky_1920x1200.jpg

Notice the -maxpect and -rmode 1 options. -maxpect expands the image to fill the screen while maintaining the aspect ratio, while -rmode 1 sets the display mode on xv to tiled. So you should now have your wallpaper displaying on both screens now (under X) without being distorted.

Here’s what it looks like on my machine:

aterm running on XP under X with a dual-monitor setup

This is on a dual-monitor setup with both screens running at 1920×1200 resolution. I’ve set X’s background to be the same as my windows Wallpaper so that it looks cooler. Notice how the background image (inside aterm) is not stretched, but tiled across the two screens. That’s all there is to it. Seems like a bit of work, but I think it’s worth it. My main reason for going through all this trouble was to get a decent terminal running in windows. I guess I could have just used xterm, but aterm looks so much nicer, doesn’t it?

Mar 272007
 

This guide is outdated. Please check out the updated version of this guide here.

On any system that I plan to use for an extended period of time, I will always install Cygwin. This is mainly because I like have UNIX tools on Windows, and also so that I can use the console to do things that DOS is not able to do. I started using Cygwin in 2000, and I’ve continued using it since. One of the cool things you can do with Cygwin is run X, which means that you can have X applications running on the Windows desktop. When I was interning at Motorola, I used to run eXceed, with fvwm. This was where I first ran into aterm. What I liked most about aterm is the eye-candy. You can have transparent windows with shading effects and all sorts of other cool stuff. I tried to get aterm running on my machine at home by compiling it from source under Cygwin. I was eventually able to do this (install libjpeg, libpng, libAfterImage, zlib, and the X includes and libraries first), but what I didn’t like was the fact that you had to start up a Cygwin console to open up X, and then aterm. I wanted aterm to start up and run directly without that ugly DOS/Cygwin console window. Of course, you can’t simply run the aterm executable because it needs X and Cygwin to be running. I eventually figured it out (actually a few months before leaving on my “extended vacation”) by starting out with X running with a rootless window. Oh, and run.exe proved to be very helpful. Anyway, here is how you do it:

First you need to add C:\cygwin\bin to your PATH Environment Variable. You can do this from My Computer > Properties > Advanced > Environment Variables. You might also have to add C:\cygwin\usr\X11R6\bin to PATH.

Then you need to create two batch files. The first one is to start X, and the second one is to start aterm (or whatever X app you want to start). The example I’m going to show includes starting up X with a wallpaper (using xv), and then running aterm. I run aterm with a transparent background, using the X wallpaper. However, you can also load aterm with a background image of your choice.

The batch file to start X, which I call xwin.bat looks like this:

C:\cygwin\usr\X11R6\bin\run.exe C:\cygwin\usr\X11R6\bin\xwin.exe -multiwindow -clipboard -silent-dup-error
C:\cygwin\usr\X11R6\bin\run.exe C:\cygwin\usr\local\bin\xv.exe -display :0 -root -quit -be -maxpect /cygdrive/c/Wallpapers/upper_limit_wp_dark_1600.jpg

This will start up X in a rootless window with upper_limit_wp_dark_1600.jpg as your X wallpaper. Next, you write a batch file (aterm.bat) that will load aterm:

C:\cygwin\usr\X11R6\bin\run.exe C:\cygwin\bin\bash.exe --login -i -c "aterm -sh 50 -tr -trsb -fade 20 -tint gray -bl -sb -st -sr -sl 1000 -tn xterm"

This batch file will load aterm with the background image at 50% brightness, transparent background, transparent scrollbar, 20% fading on losing focus, gray tint, borderless window (sometimes works), scrollbar, trough-less scrollbar, scrollbar on the right, 1000 scrollback lines, and with xterm terminal emulation. One issue I have had with this, is that aterm may load up with the default (checkered) X background. This is because the xv did not properly execute in xwin.bat. I have no idea why this happens, sometimes it works, and sometimes it doesn’t. If it doesn’t, you can modify aterm.bat:

C:\cygwin\usr\X11R6\bin\run.exe C:\cygwin\bin\bash.exe --login -i -c "xv -display :0 -root -quit -be -max /cygdrive/c/Wallpapers/upper_limit_wp_dark_1600.jpg &#38;&#38; aterm -sh 50 -tr -trsb -fade 20 -tint gray -bl -sb -st -sr -sl 1000 -tn xterm"

This batch file will load xv every time you start aterm, so there is a slight performance hit on startup. However, it’s not that big of a deal because the xv instance quits right after it sets up the wallpaper, and so you’re not loading a new instance of xv into the memory every time.

Well, there you have it. I hope it was helpful!

X with XP
Screenshot of my XP desktop, with aterm, xcalc, xclock, xeyes, and xterm running

Aug 232005
 

Google has come out with their instant messaging program. This seems to be unofficial; the only mentions I can see are on this Slashdot article, and the website I linked to.

You should be able to get it to work using any Jabber client. I’ve been trying to get it to work on Trillian, but I’ve been having some trouble. I’ve set my username as [my gmail id]@gmail.com and the server as talk.google.com. Trillian authenticates, but then quits after receiving “iq errors”:

[Tuesday, August 23, 2005 - 17:04:55] *** Creating connection "[my gmail id]@gmail.com/"
[Tuesday, August 23, 2005 - 17:04:56] *** Server supports TLS encryption...
[Tuesday, August 23, 2005 - 17:04:56] *** Negotiating XMPP SSL connection...
[Tuesday, August 23, 2005 - 17:04:57] *** Connection established using EDH-RSA-DES-CBC3-SHA (TLSv1/SSLv3)
[Tuesday, August 23, 2005 - 17:05:05] *** Attempting to authenticate using PLAIN
[Tuesday, August 23, 2005 - 17:05:06] *** Authenticated.
[Tuesday, August 23, 2005 - 17:05:15] *** You have successfully connected to Jabber.
[Tuesday, August 23, 2005 - 17:05:15] *** ERROR: iq error 501
[Tuesday, August 23, 2005 - 17:05:15] *** Retrieved user roster from server.
[Tuesday, August 23, 2005 - 17:05:16] *** ERROR: iq error 501
[Tuesday, August 23, 2005 - 17:05:16] *** ERROR: iq error 503 from gmail.com
[Tuesday, August 23, 2005 - 17:05:16] *** ERROR: iq error 503 from gmail.com

If anyone has had anymore success, please let me know. I’m not sure how popular this is going to be; people usually tend to stay within a particular “IM clique” (with AIM, MSN Messenger, and Yahoo! Chat being the major players). I use Trillian so that I can talk people in any one of these “cliques”.

This ends months of speculation and rumours about Google’s foray into the IM arena. I wonder what’s next?

UPDATE #1

I tried it again, this time with “Trillian” in the “Resource” field, no luck – Trillian still loses connection to the server. It comes up with a bunch of “Connection lost to server” errors.

[Tuesday, August 23, 2005 - 17:44:00] *** Creating connection "[my gmail id]@gmail.com/Trillian"
[Tuesday, August 23, 2005 - 17:44:01] *** Server supports TLS encryption...
[Tuesday, August 23, 2005 - 17:44:01] *** Negotiating XMPP SSL connection...
[Tuesday, August 23, 2005 - 17:44:02] *** Connection established using EDH-RSA-DES-CBC3-SHA (TLSv1/SSLv3)
[Tuesday, August 23, 2005 - 17:44:10] *** Attempting to authenticate using PLAIN
[Tuesday, August 23, 2005 - 17:44:16] *** Authenticated.
[Tuesday, August 23, 2005 - 17:44:21] *** Connection lost to server.
[Tuesday, August 23, 2005 - 17:44:21] *** Connection lost to server.
[Tuesday, August 23, 2005 - 17:44:21] *** Connection lost to server.
[Tuesday, August 23, 2005 - 17:44:21] *** Connection lost to server.
[Tuesday, August 23, 2005 - 17:44:22] *** Connection lost to server.
[Tuesday, August 23, 2005 - 17:44:22] *** Connection lost to server.
[Tuesday, August 23, 2005 - 17:44:22] *** Connection lost to server.

UPDATE #2

I found this site that has instructions on getting Google IM to work through Jabber on Trillian. It has the same settings that I use, but they claim to have gotten it to work; although I see the same “Connection lost to server” errors on their status window as well. I also noticed that their status window shows the authentication being done with “username@talk.google.com” as oppsed to “username@gmail.com”, which is what I’m seeing.

UPDATE #3

Success (sort of)! It works now – I didn’t do anything different, I tried disconnecting and reconnecting, and now the connection seems to be stable. However, I’m unable to add anyone, or IM them. I tried adding myself, but was unable to do so. I’ll keep playing around anyway… Here are a few screenshots:

Manage Connections Preferences Status Window

UPDATE #4

It looks like it’s working now – I still lose connection occasionally, but it seems much more stable than before. I have also been able to add people and IM them.

Apr 292005
 

Firefox, the Open Source browser has hit 50 million downloads! Absolutely amazing!

I remember when I used to use Internet Explorer because I didn’t think there were any good browsers out there. I also remember when I even flamed Mozilla (I knew this would bite me in the ass later), but that was because this guy I didn’t like, who I used to work with, used it and it was kinda like a “guilty by association” thing. But anyway, this right here, is really noteworthy. I don’t think any other Open Source project has even grabbed the attention of your every day computer user as much as Firefox has.

Ever since I downloaded Firefox I’ve had a markedly better browsing experience. No pop-ups, no browser hijacks, and tabbed browsing! I don’t even know how I got along without it! Firefox wins hands down over IE with its simplicity, speed, functionality and inuitive browsing experience. Why don’t you Open Source critics take a look at this? If they think Open Source can’t produce anything worthwhile, they should take a look at this. This is what Open Source can achieve. Firefox is well on its way to being a household name.

Congratulations to the Firefox team! Also, if you don’t have Firefox, go and get it now!

All original content on these pages is fingerprinted and certified by Digiprove