Thursday, November 15, 2007

Javascript scoping problems? Is it Firefox, IE or... Prototype?

I was asked to assist a friend who was experiencing some unusual behaviour in his web application. While it worked as expected in Internet Explorer, it was failing in Firefox, so he asked me to have a look.

The relevant source code:

<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<script src="inc/third_party/prototype/prototype.js" type="text/javascript"></script>
<script src="inc/third_party/tooltip.js" type="text/javascript"></script>
<script src="inc/third_party/protoload.js" type="text/javascript"></script>
</head>

<body>
<div id="main">
<script type="text/javascript">
function update(orderby,sort,page) {
$("80").update("<center><img src=\"images/loading/waiting.gif\"/></center>");
var request = "module/80.php?orderby="+orderby+"&page="+page+"&sort="+sort+"&status_id=80";
new Ajax.Updater("80", request, { method: "get" });
}

new Ajax.Updater("80", "module/80.php?status_id=80", { method: "get" });

</script>

<div id=80>
<img src="images/loading/waiting.gif"/>
</div>
</div>
</body>
</html>

The loading AJAX imported a table with headers like this:



Which rendered like this:



The expected behaviour was that clicking the header would pull in an appropriately sorted table via an Ajax request, and it worked as expected in IE7.

In Firefox however, clicking a header simply replaced the header with the orderby value being passed in the update() function:



Several minutes messing around with Firebug showed that it should work as written, although I could never get my breakpoints to fire off. (But as I haven't yet fully mastered Firebug, I put this down to my lack of experience)

As I sat there stumped, a vague sense of déjà vu crept over me, and I suggested to my friend that he rename update() to something very unique.

Which promptly resolved the issue. So, it was obviously a problem with namespace pollution... although I use the term 'namespace' liberally. Problem is, I can't replicate this using normal Javascript - that is, inline scripts will override external Javascript anytime.

So, that leaves one option - Prototype is extending HTMLElement or similar with an update() method. After a bit of digging, my suspicions are confirmed

In browsers that support adding methods to prototype of native objects such as HTMLElement all DOM extensions on the element are available by default without ever having to call Element.extend(), dollar function or anything! ...Because the prototype of the native browser object is extended, all DOM elements have Prototype extension methods built-in. This, however, isn't true for IE which doesn't let anyone touch HTMLElement.prototype.
That's one hell of a gotcha, and all to avoid using $()... I don't know if protecting HTMLElement.prototype is correct behaviour or not, but I'm now convinced that attaching your methods to it is very much incorrect.

The Prototype documentation on this 'feature' states:
This document reveals some clever hacks found in Prototype.

Clever? Very clever, yes. Hacks? Most definitely.

Monday, June 04, 2007

An unscientific comparison 3.5 - getting the cookie, Haskell

Part 3.5 of N in a series. Click here for Part 3.4

Wow. This was incredibly hard.

Like, ludicrously hard. Nothing I've done before really prepared me for it. I thought I would be okay, I mean, pattern matching, no side effects? I loved that stuff in Erlang, bring it on! But then... then I met the typing system, and monads. Oh boy.

Before I get to the code, I'd like to thank Porges for his patient assistance with my struggling towards glimmers of comprehension. The file functions are his, as is the fact that my getCookie function works - he taught me a lot about monads in the process. :)


1 module FirstCookie where
2
3 import Network.HTTP
4 import Network.Browser
5 import Network.URI
6
7
8
9 getCookie user pwd uri ua = do
10 redir <- browse $ getCookie' user pwd uri ua
11 browse $ getCookie'' redir
12
13 getCookie' user pwd uri ua = do
14 setAllowRedirects False
15 let (Just parsed_uri) = parseURI uri
16 let f = Form POST parsed_uri [("id",user), ("pwd",pwd), ("dologin", "yes")]
17 (uri,resp) <- request $ formToRequest f
18 let (Just redirectLocation) = findHeader HdrLocation resp
19 let (Just parsed_redir_uri) = parseURI redirectLocation
20 return parsed_redir_uri
21
22 getCookie'' uri = do
23 request $ defaultGETRequest uri
24 getCookies
25
26
27 --readCookie takes a file path and returns
28 -- a Cookie read from that file.
29 --
30 --read converts something from string representation
31 readCookie :: FilePath -> IO Cookie
32 readCookie f = do
33 -- read in the contents of the file (<-: extract string from IO monad)
34 contents <- readFile f
35 -- make a cookie from the contents
36 let cookie = read contents
37 -- return the cookie wrapped in the IO monad
38 return cookie
39
40 --writeCookie takes a file path and a cookie and writes it
41 --
42 --show converts something to string representation
43 writeCookie :: FilePath -> Cookie -> IO ()
44 writeCookie f c = writeFile f (show c)
Comments

I found this unbelievably hard at first. Why? Two things, monads and typing.

Monads
According to Wikipedia, a monad is a "construction that, given an underlying type system, embeds a corresponding monadic type system into it"... which really explains it, no? There's a thousand metaphors to try and explain them.

In the course of writing these functions, I had dealings with the following monads: Maybe, BrowserAction and IO. Here's my understanding of each (which could be entirely wrong).

  • Maybe is a wrapper of sorts to allow polymorphic returns from functions that could fail, while retaining determinism. It seems well suited for chains of functions, where any function could fail and return Nothing. Maybe seems to remove the necessity for long manual chains of pattern matching and function calls. Maybe is also a bit like Schrodinger's Cat - until you look at it, you can't tell what the result was, hence 'extracting values' by using pattern matching to bind - that "(Just x)" stuff. Although I'm not sure quite what kind of entity Just is. Googling's not helping.
  • BrowserAction holds state for the Browser module - from the API documents, it looks like every stateful function returns an empty BrowserAction... not quite sure how it works, but at least I know how and where the BrowserAction is coming from.
  • IO is also stateful as far as I'm aware. I'm not sure how they achieve that, and would like to view the code they generate one day, like using macroexpand in Lisp, although I'm assured that it'll be a whole lot of ugly looking lambdas. But still, curiosity and all.

The biggest problem with monads? My preconceptions about monads. I've been prepped to think that monads are confusing and complicated by reading about how confusing and complicated they are. When in reality, they're merely different. The term "monad" doesn't help, sounds quite daunting and theoretical. I'm trying to substitute the term "wuzzle" in my thinking about them, to make them sound cute and fuzzy... but Maybe wuzzle sounds kind of silly.

But anyway, as is probably evident to any experienced Haskell hackers reading, I still have a very imperfect comprehension of how monads fit into the scheme of things, but they're not so scary now.

Typing
Going hand in hand with my lack of understanding about monads was troubles with types. But they tie in together, after all, a monad is simply a construction that, given an underlying type system, embeds a corresponding monadic type system into it... but yes, realising a monad is a type was one of those minor epiphany moments. My main issue seems to be understanding the type notation used. I guess it's a case of practise makes perfect. I'm also unused to using a static typing system where I'm not casting all over the show, makes me feel uneasy.

Documentation
I could've killed for some more indepth examples when I was writing this. I find API documents are great when you know what you're doing and just want to check you're passing the right arguments, but when you're trying to figure out how to do task X, API documents at best lend clues. A few examples and/or summary explaining the intended usage can be lifesavers.

Overall
Haskell feels like it's adhering to a theory (category theory?) - this manifests in behaviour that may not be pragmatic, but it feels consistent with itself - the rules that govern Haskell, whatever they are, make themselves known in the "texture" of the language - I'm afraid I can't explain it better than that, it's more of an intuitive thing. I imagine that using Haskell would reveal the nature of this theory in a manner a mathematical layman like myself can grasp... but I also feel that a grasp of the theory would make using Haskell a lot easier.

An unscientific comparison 3.4 - getting the cookie, Ruby

Part 3.4 of N in a series. Click here for Part 3.3.2

Well, my first Ruby in a while, and it was certainly different to what I was expecting. I finally figured out why people like certain aspects of it, but other aspects left me cold.

But first, the code.


1 require 'net/http'
2 require 'uri'
3
4
5 def getCookie(user, pwd, uri, userAgent)
6 url = URI.parse(uri)
7 request = Net::HTTP::Post.new(url.path)
8
9 request.set_form_data({ "id" => user, "pwd" => pwd, "dologin" => "yes"})
10 request.add_field("User-Agent", userAgent)
11
12 response = Net::HTTP.new(url.host).start {|http| http.request(request) }
13 cookieUrl = URI.parse(response.fetch("location"))
14
15 cookieRequest = Net::HTTP.new(cookieUrl.host)
16 cookieResponse = cookieRequest.get(cookieUrl.path + "?" + cookieUrl.query,
{
"User-Agent" => userAgent})
17
18 return cookieResponse.fetch("set-cookie")
19 end
20
21 def storeCookie(cookie, filePath)
22 File.open(filePath, "w") do |f|
23 f.puts cookie
24 end
25 end
26
27 def retrieveCookie(filePath)
28 f = File.open(filePath, "r")
29 cookie = f.gets
30 f.close
31 return cookie
32 end
Comments

Documentation
Maybe I'm looking in the wrong place, but Ruby documentation, to be frank, is terrible. The documentation that shipped with my Ruby install is out of date, and the online documentation is an API reference - but it's one of the most confusing API references I've ever used. For example, clicking on Net::HTTP::Post under the classes list is not terribly illuminating.

TMTOWDI
I have nothing against multiple ways to perform the same task - except when it leads to multiple partially implemented ways to perform the same task. For example, POSTing.

There is a method Net::HTTP.post which works almost identically to my usage of the get method above. Would've shortened the code considerably. Except, if I can quote the docs:
Posts data (must be a String) to path.
So, I can use the shorter and simpler method, but I need to convert the data into an urlencoded string myself before using it, or I can use the Request object which encodes the data for me, but has a more cumbersome method of dispatching that request.

URI class
It's normal to use a class to represent a URI, but is there a way to get my whole URI (including the query string I passed in originally) out? uri.path + "?" + uri.query feels a bit hackish.

Blocks
Blocks are awesome. Python needs to steal these asap, single line lambdas don't count. One thing I gained from this was an appreciation for blocks and what they're good for. (Also, symbols. Yay for symbols, not that I used them here.)

Overall
I actually might try and find a third party improvement on this lot. The thought of using Net::HTTP again is a tad demotivational. Documentation is terrible - but I suspect that's because for most of Ruby's life, it's been used mainly by Japanese speaking people, so hasn't developed the mature English documentation yet. To any Ruby documentation writers reading: "More examples please." I'm a sucker for examples.

Click here for Part 3.5

Friday, May 25, 2007

An unscientific comparison 3.3.2 - Getting the cookie, C# final

Part 3.3.2 of N in a series - click here for Part 3.3.1

So, final version. After my embarrassing myself with my initial approach and associated tantrum, I thought I better do it in a slightly saner fashion - don't want to have eaten humble pie for nothing.


    1 using System;
    2 using System.Collections.Generic;
    3 using System.Collections.Specialized;
    4 using System.Text;
    5 using System.Net;
    6 using System.IO;
    7 using System.Runtime.Serialization;
    8 using System.Runtime.Serialization.Formatters.Binary;
    9 
   10 namespace firstCookie3
   11 {
   12     class CustomWebClient : WebClient
   13     {
   14         public event Action<WebRequest> ModifyRequest;
   15 
   16         public CustomWebClient() : base() { }
   17 
   18         protected override WebRequest GetWebRequest(Uri address)
   19         {
   20             WebRequest request = base.GetWebRequest(address);
   21             OnModifyRequest(request);
   22             return request;
   23         }
   24 
   25         private void OnModifyRequest(WebRequest request)
   26         {
   27             if (ModifyRequest != null)
   28                 ModifyRequest(request);
   29         }
   30 
   31     }
   32 
   33     class CookieFunctions
   34     {
   35         public static CookieCollection GetCookie(string user, string pwd, 
string
uri, string ua)
   36         {
   37             NameValueCollection postData = new NameValueCollection();
   38             CookieContainer ckCont = new CookieContainer();
   39             CustomWebClient client = new CustomWebClient();
   40 
   41             postData["id"] = user;
   42             postData["pwd"] = pwd;
   43             postData["dologin"] = "yes";
   44             client.ModifyRequest += delegate(WebRequest req)
{
((HttpWebRequest)req).CookieContainer = ckCont;
};
   45             client.ModifyRequest += delegate(WebRequest req)
{
((HttpWebRequest)req).UserAgent = ua;
};
   46             client.UploadValues(uri, postData);
   47 
   48             return ckCont.GetCookies(new Uri(uri));
   49         }
   50 
   51         public static void StoreCookie(CookieCollection ck, string filePath)
   52         {
   53             IFormatter formatter = new BinaryFormatter();
   54             Stream outStream = new FileStream(filePath,
   55                                               FileMode.Create,
   56                                               FileAccess.Write,
   57                                               FileShare.None);
   58             formatter.Serialize(outStream, ck);
   59             outStream.Close();
   60         }
   61 
   62         public static CookieCollection RetrieveCookie(string filePath)
   63         {
   64             IFormatter formatter = new BinaryFormatter();
   65             Stream iStream = new FileStream(filePath,
   66                                             FileMode.Open,
   67                                             FileAccess.Read,
   68                                             FileShare.Read);
   69             CookieCollection ck = (CookieCollection)formatter.Deserialize(iStream);
   70             return ck;
   71         }
   72 
   73 
   74     }
   75 }

The only comment I have to make is this - I'm glad they left that little hook there for me, but what worries me is the fact that I'm dependent on them doing so. I now understand what it's like to work in a B&D language.

Click here for Part 3.4

An unscientific comparison 3.3.1 - Getting the cookie, C# - a better way

Part 3.3.1 of N in a series. Click here for Part 3.3.

Well, after much moaning and complaining and cursing of Bill Gates, I've decided that I need to update the C# attempt. Turns out, there's an easy way in .NET, and there's the horribly hard way.
The easy way is to work with the library. The hard way is to to brute force it. Guess what my category my last attempt fell under.

But let me start by saying that I probably would've left it at that if not for the patient guidance of these gentlemen - linkdown, porges, and a fellow by the nick of Kanibiss. So thanks guys.

So, a dissection of where I went wrong... well basically, it was the dismissal of System.Net.WebClient as being "too high level"... ah, hubris, my eternal enemy. Now I knew that to collect cookies I'd need to add a CookieContainer to an HttpWebRequest; my error was in assuming that WebClient didn't expose this in any way that I could use.

WebClient contained all the functionality I had replicated in my HttpWebRequestWrapper - URI encoding, setting of headers, etc, butI just couldn't figure out how to set a CookieContainer and without doing so, cookies aren't retained.

I had actually seen the method WebClient.GetWebRequest in the documentation, but had dismissed it - after all, it only returned a WebRequest object, it didn't return a reference to the WebRequest object being used by WebClient. The documentation contained a code sample showing exactly what I needed to do, but my inexperience tripped me up here and I totally missed the import of it.

After linkdown kept prompting me to investigate further, I downloaded Reflector and had a closer look at WebClient. Specifically, the UploadValues method, as this was the one I would like to have used... and lo and behold, the way was revealed.

WebRequest request = null;
this.ClearWebClientState();
try
{
    byte[] buffer = this.UploadValuesInternal(data);
    this.m_Method = method;
    request = this.m_WebRequest = this.GetWebRequest(this.GetUri(address));
    this.UploadBits(request, null, buffer, null, null, null, null);
    byte[] buffer2 = this.DownloadBits(request, null, null, null);
    if (Logging.On)
    {
        Logging.Exit(Logging.Web, this, "UploadValues", address + ", " + method);
    }
    buffer3 = buffer2;
}

The relevant bit is the call to... GetWebRequest. Ah. It was then that I noted that GetWebRequest was designated as virtual - that is, as overrideable by subclasses of WebClient.

A minor epiphany occurred - the designers of this class had left me a hook, as it were, but I was too inexperienced to see it. I felt very stupid and elated at the same time.

I quickly sat down and hacked out some code to use it, and then deleted it and rewrote it to allow for generalised usage - my first attempt looked like this -

    1 using System;
    2 using System.Collections.Generic;
    3 using System.Text;
    4 using System.Net;
    5 
    6 
    7 namespace firstCookie2
    8 {
    9     public delegate void ModifyRequest(WebRequest w);
   10 
   11 
   12 
   13 
   14     class CustomWebClient : WebClient
   15     {
   16         Queue<modifyrequest> DelegateQueue;
   17 
   18         public CustomWebClient() : base()
   19         {
   20             DelegateQueue = new Queue();
   21         }
   22 
   23         protected override WebRequest GetWebRequest(Uri address)
   24         {
   25             WebRequest request = base.GetWebRequest(address);
   26             ApplyDelegates(request);
   27             return request;
   28         }
   29 
   30         private WebRequest ApplyDelegates(WebRequest request)
   31         {
   32             while (DelegateQueue.Count > 0)
   33             {
   34                 ModifyRequest modDelegate = DelegateQueue.Dequeue();
   35                 modDelegate(request);
   36             }
   37             return request;
   38         }
   39         public void AddDelegate(ModifyRequest del)
   40         {
   41             DelegateQueue.Enqueue(del);
   42         }
   43 
   44     }
   45 }



After some sage advice from this fine fellow, I modified the class to use events:


   26 class CustomWebClient : WebClient
   27 {
   28     public event Action<WebRequest> ModifyRequest;
   29 
   30     public CustomWebClient() : base() { }
   31 
   32     protected override WebRequest GetWebRequest(Uri address)
   33     {
   34         WebRequest request = base.GetWebRequest(address);
   35         OnModifyRequest(request);
   36         return request;
   37     }
   38 
   39     private void OnModifyRequest(WebRequest request)
   40     {
   41         if (ModifyRequest != null)
   42             ModifyRequest(request);
   43     }
   44 
   45 }



Which can then be used like so:


static void Main(string[] args)
{
    CustomWebClient w = new CustomWebClient();
    CookieContainer cj = new CookieContainer();
    w.ModifyRequest += delegate(WebRequest request)
    {
        ((HttpWebRequest)request).CookieContainer = cj;
    };
    w.ModifyRequest += delegate(WebRequest request)
    {
        ((HttpWebRequest)request).AllowAutoRedirect = true;
    };
    string v = w.DownloadString("http://www.google.co.nz");
}



So. Now I get to rewrite the rest of my C# piece. I'm very embarrassed at my mistake, but I learnt a lot about .NET in the process. I also have a far better vehicle for my HTTP requests now, far more flexible and far more powerful. My thanks to all who gave advice. :)

Click here for Part 3.3.2

Sunday, May 20, 2007

An unscientific comparison 3.3 - Getting the cookie, C#

Part 3.3 of N in a series. Click here for part 3.2.

Wow, this was unexpectedly unpleasant. After a few months of using C#, I'd grown quite fond of it and .NET - that fondness got a battering tonight.

And all I wanted to do was subclass System.Net.HttpWebRequest. But first, the code.

    1 using System;
    2 using System.Collections.Generic;
    3 using System.Text;
    4 using System.Net;
    5 using System.Web;
    6 using System.IO;
    7 using System.Runtime.Serialization;
    8 using System.Runtime.Serialization.Formatters.Binary;
    9 using StringHashTable = System.Collections.Generic.Dictionary<string, string>;
   10 using StringList = System.Collections.Generic.List<string>;
   11 
   12 namespace firstCookie
   13 {
   14     class HttpRequestWrapper
   15     {
   16         HttpWebRequest request;
   17         StringHashTable postData;
   18 
   19         public HttpRequestWrapper(string uri)
   20         {   
   21             request = (HttpWebRequest)WebRequest.Create(uri);
   22 
   23         }
   24 
   25         public HttpWebResponse Post()
   26         {
   27             if (request.Method != "POST") request.Method = "POST";
   28 
   29             /*Prepare data - If I haven't actually set the data,
then I deserve an exception*/
   30             byte[] preppedData = prepareData(postData);
   31             request.ContentType = "application/x-www-form-urlencoded";
   32             request.ContentLength = preppedData.Length;
   33 
   34             //Now POST
   35             Stream postStream = request.GetRequestStream();
   36             postStream.Write(preppedData, 0, preppedData.Length);
   37             return (HttpWebResponse)request.GetResponse();
   38         }
   39 
   40         private byte[] prepareData(StringHashTable postData)
   41         {
   42             //Firstly, convert data to usual query string form.
   43             string formatString = "{0}={1}";
   44             StringList pairsList = new StringList();
   45             foreach (string key in postData.Keys)
   46             {
   47                 string encodedKey = HttpUtility.UrlEncode(key);
   48                 string encodedValue = HttpUtility.UrlEncode(postData[key]);
   49                 string encodedString = String.Format(formatString,
encodedKey,
encodedValue);
   50                 pairsList.Add(encodedString);
   51             }
   52             string queryString = String.Join("&", pairsList.ToArray());
   53 
   54             /*Now, convert string to byte[] - it's needed in this format,
can't quite say why.*/
   55             ASCIIEncoding byteEncoder = new ASCIIEncoding();
   56             byte[] encodedData = byteEncoder.GetBytes(queryString);
   57             return encodedData;
   58         }
   59 
   60         public StringHashTable formData
   61         {
   62             //Futureproof this, my Python side just wants to make postData public.
   63             get { return postData; }
   64             set { postData = value; }
   65         }
   66 
   67         public string Method
   68         {
   69             get { return request.Method; }
   70             set { request.Method = value; }
   71         }
   72 
   73         public string UserAgent
   74         {
   75             get { return request.UserAgent; }
   76             set { request.UserAgent = value; }
   77         }
   78 
   79         public CookieContainer CookieContainer
   80         {
   81             get { return request.CookieContainer; }
   82             set { request.CookieContainer = value; }
   83         }
   84 
   85         public bool AllowAutoRedirect
   86         {
   87             get { return request.AllowAutoRedirect; }
   88             set { request.AllowAutoRedirect = value; }
   89         }
   90 
   91     }
   92 
   93     class RequestFunctions
   94     {
   95        public static HttpRequestWrapper CreateRequest(string uri, string userAgent)
   96         {
   97             HttpRequestWrapper req = new HttpRequestWrapper(uri);
   98             req.UserAgent = userAgent;
   99             req.Method = "GET";
  100             return req;
  101         }
  102 
  103         public static HttpRequestWrapper CreateRequest(string uri,
StringHashTable data,
string
userAgent)
  104         {
  105 
  106             HttpRequestWrapper req = CreateRequest(uri, userAgent);
  107             req.Method = "POST";
  108             req.formData = data;
  109             return req;
  110         }
  111 
  112 
  113     }
  114 
  115 
  116 
  117     class CookieFunctions
  118     {
  119         public static CookieCollection GetCookie(string user, 
string
pwd,
string
uri,
string ua)
  120         {
  121             StringHashTable postData = new StringHashTable();
  122             postData["id"] = user;
  123             postData["pwd"] = pwd;
  124             postData["dologin"] = "yes";
  125 
  126             //Create request and prepare Data
  127             HttpRequestWrapper request = RequestFunctions.CreateRequest(uri,
postData,
ua);
  128 
  129             //Last tweaks to the request
  130             CookieContainer ckCont = new CookieContainer();
  131             request.CookieContainer = ckCont;
  132             request.AllowAutoRedirect = true;
  133             HttpWebResponse response = request.Post();
  134             return ckCont.GetCookies(new Uri(uri));
  135         }
  136 
  137         public static void StoreCookie(CookieCollection ck, string filePath)
  138         {
  139             IFormatter formatter = new BinaryFormatter();
  140             Stream outStream = new FileStream(filePath,
  141                                               FileMode.Create,
  142                                               FileAccess.Write,
  143                                               FileShare.None);
  144             formatter.Serialize(outStream, ck);
  145             outStream.Close();
  146         }
  147 
  148         public static CookieCollection RetrieveCookie(string filePath)
  149         {
  150             IFormatter formatter = new BinaryFormatter();
  151             Stream iStream = new FileStream(filePath,
  152                                             FileMode.Open,
  153                                             FileAccess.Read,
  154                                             FileShare.Read);
  155             CookieCollection ck = (CookieCollection)formatter.Deserialize(iStream);
  156             return ck;
  157         }
  158 
  159 
  160 
  161 
  162 
  163     }
  164 
  165 
  166 }


Comments


You take the low road, and I'll take the high road...
.NET offers two ways of retrieving remote websites via HTTP. System.Net.HttpWebRequest and System.Net.WebClient. WebClient is very high level and handy for simple tasks; unfortunately, storing cookies isn't something it can do. This is where HttpWebRequest comes in.
It can perform the tasks needed, but not overly well. To be honest, I was a bit appalled at how limited HttpWebRequest was.

Url encoding query strings
HttpWebRequest can't do this. The only functionality for encoding uri strings is located in System.Web.HttpUtility.UrlEncode - System.Web is aimed at server side ASP.NET more than client side stuff like this and requires that you add a reference to the assembly. It's somewhat limited - it can only encode a string - so passing in mappings of names to values and getting an encoded string returned is not possible.

Sending the POST data
Once again, HttpWebRequest doesn't know how to do this. I'm sure there are very good reasons why not, but it's a bit frustrating. So, once I've taken my data, and uri encoded it, I then have to convert it to a series of bytes to POST to the server.

Subclassing HttpWebRequest
And this is where I decided I was going to subclass. All I wanted to add was a method to encode a generic Dictionary passed to it, and a method to handle the minor details of actually POSTing the data. And that's when I learnt about the internal keyword.
HttpWebRequest has multiple constructors. The one I needed to call was marked internal - that is, it can only be called by code residing in the same assembly. One of the constructors is accessible, but it's an obsolete one that provides no required functionality. I ended up going with the wrapper approach - which feels like an ugly hack, but will save me from replicating the same code over and over as I proceed.

Overall
I was quite disappointed with HttpWebRequest in some places - in others, I was pleased, for example, AllowAutoRedirect is a nice touch, in Python's urllib2, for example, you have no choice. Serialising objects was straightforward, and the documentation, overall, was reasonably thorough. Some more relevant examples would always be nice, but that's MSDN for you.

I'm still not happy about having to wrap a class when subclassing it would've been a lot simpler and neater - it's a bit of a horrible hack, because each time I need another property of HttpWebRequest, I'll have to add another property to HttpRequestWrapper.

Click here for Part 3.3.1

Friday, May 18, 2007

An unscientific comparison 3.2 - Getting the cookie, Scheme

Part 3.2 of N in a series. Click here for part 3.1.

I must admit, writing this code took me the best part of an evening - mainly because of my inexperience in Scheme. I'm fine with prefix notation and the parentheses for Africa, it's merely that I lack the background knowledge to use Scheme properly - the background knowledge of idioms and usage patterns that one tends to acquire by osmosis. I'd like to comment that the residents of #Scheme on the Freenode IRC network are incredibly helpful, and very patient with dumb newbie question.

So, the code using Felix Winkelmann's http library for Chicken Scheme.

1 (require-extension http-client)
2 (require-extension srfi-13) ;string functions
3 (require-extension srfi-8) ;receive statement
4 (require-extension srfi-16) ;case-lambda
5
6 (define create-request
7 (case-lambda
8 ((method uri user-agent) (http:make-request method
uri
`
(("User-Agent" . ,user-agent))))
9 ((method uri data user-agent)
10 (let ((encoded-data (uri-encode-query data)))
11 (http:make-request method
12 uri
13 `(("User-Agent" . ,user-agent)
14 ("Content-Type" . "application/x-www-form-urlencoded")
15 ("Content-Length" . ,(string-length encoded-data)))
16 encoded-data)))))
17
18
19 (define (get-cookie user pwd uri user-agent)
20 (let* ((data `(("id" . ,user) ("pwd" . ,pwd) ("dologin" . "yes")))
21 (auth-req (create-request 'POST uri data user-agent)))
22 (receive (a header-list b c) (http:send-request auth-req) ;Request A
23 (let ((cookie-req (create-request 'GET
24
(alist-ref "location"
header-list
equal?
)
25 user-agent)))
26 (http:GET cookie-req) ;Request B
27 (alist-ref "Cookie" (http:request-attributes cookie-req) equal?)))))
28
29
30 (define (store-cookie cookie file-path)
31 (let ((f (open-output-file file-path)))
32 (write cookie f)
33 (close-output-port f)))
34
35
36 (define (retrieve-cookie file-path)
37 (let* ((f (open-input-file file-path))
38 (cookie (read f)))
39 (close-input-port f)
40 cookie))
Comments

Documentation
The documentation for the http library covers all the bases - my only quibble would be a lack of examples, but that's a tiny quibble that arises only from my inexperience.

Data structures
I had a bit of hassle with using alists - the default predicate for equivalence used by the built-in function for alist item retrieval, assv, is eqv? - which returns false for "test" == "test". Chicken Scheme comes with some extra functions that I'd most likely end up writing if using R5RS Scheme, one of which is alist-ref, which allows me to specify that the predicate to be used is equal? which returns true for "test" == "test". (And also for "TEST" == "test", this could be problematic in certain situations, but is fine here) Learning how to compose an alist inline was interesting, backquoting and comma prefixing all over the show.

File IO
Reading and writing to/from a file was relatively painless compared to my experiences in Lisp.

SRFIs
The SRFIs are great, but a little obscure at first. It's become clear to me that I'll need to become familiar with their contents if I'm to make effective use of Scheme.

Overall
Using the http library ended up being a little lower level than I had anticipated, mainly because my usage fell outside of the common usages for which convenience functions existed - for instance, instead of building a request for posting and setting Content-Length headers myself, I could've used http:POST, which would've done all of that, but it only returns the body of the response, when I needed the header to follow the redirect. (Request A returns a 302 response, request B actually gets the cookie.) I did note that http:POST doesn't urlencode the datapassed to it, however, and I had to write a wee socket server in Erlang to confirm that. Not a bug, just something to note.

I learnt a fair bit about Scheme in doing this, and the support from the Scheme IRC residents on Freenode, both #Scheme and #Chicken, was fantastic. I quite enjoyed myself, and this is exactly why I chose this method - "sink or swim" seems to be the best way to learn a language and its environment quickly.

Click here for Part 3.3