1 /**
  2  * @fileOverview A collection of miscellaneous utilities.
  3  */
  4 
  5 if (!appjet) {
  6   throw new Error('appjet library is required for util library.');
  7 }
  8 
  9 /**
 10  * Returns a string version of the MD5 signature of x.
 11  *
 12  * @example print(md5("appjet")); // prints "9b458805f67473b49761c13e48c5de35"
 13  * 
 14  * @param {String} x a string
 15  * @return {String} the md5 hash of x
 16  */
 17 function md5(x) {
 18   return appjet._native.md5(x);
 19 }
 20 
 21 /**
 22  * Iterator convenience for JavaScript Objects.
 23  *
 24  * Note that if func returns false, the iteration will be immediately terminated.
 25  * (Returning undefined, or not specifying a return type, does not terminate the iteration).
 26  *
 27  * @example
 28 var pastels = {
 29   red: "#fcc",
 30   green: "#cfc",
 31   blue: "#ccf"
 32 };
 33 eachProperty(pastels, function(key, value) {
 34   print(DIV({style: 'background: '+value+';'}, key));
 35 });
 36  *
 37  * @param {object} obj The object over which to iterate.
 38  * @param {function} func The function to run on each [key,value] pair.
 39  */
 40 function eachProperty(obj, func) {
 41   var r;
 42   for (k in obj) {
 43     if (obj.hasOwnProperty(k)) {
 44       r = func(k,obj[k]);
 45       if (r === false) {
 46 	break;
 47       }
 48     }
 49   }
 50 }
 51 
 52 /**
 53  * Douglas Crockford's "object" function for prototypal inheritance, taken from
 54  * http://javascript.crockford.com/prototypal.html
 55  *
 56  * @param {object} parent The parent object.
 57  * @return {object} A new object whose prototype is parent.
 58  */
 59 function object(parent) {
 60   function f() {};
 61   f.prototype = parent;
 62   return new f();
 63 }
 64 
 65 /**
 66  * Creates an array of the properties of <code>obj</code>,
 67  * <em>not</em> including built-in or inherited properties.  If no
 68  * argument is given, applies to the global object.
 69  *
 70  * @example
 71 // Prints "abc"
 72 keys({a: 1, b: 2, c: 3}).forEach(function(k) {
 73   print(k);
 74 }
 75  *
 76  * @example
 77 // Prints all the functions and object members of the global "appjet" object,
 78 // one per line.
 79 print(keys(appjet).join('\n'));
 80  *
 81  * @param {object} obj
 82  */
 83 function keys(obj) {
 84   var array = [];
 85   var o = obj;
 86   if (o == undefined) {
 87     o = this;
 88   }
 89   for(var k in o) {
 90     if (o.hasOwnProperty(k)) {
 91       array.push(k);
 92     }
 93   }
 94   return array;
 95 }
 96 
 97 /**
 98  * Comparator that returns -1, +1, or 0 depending on whether a<b, or a>b, or
 99  * neither, respectively.
100  * @param {object} a
101  * @param {object} b
102  * @return {number} -1, 0, or +1
103  */
104 function cmp(a,b) {
105   if (a < b) {
106     return -1;
107   }
108   if (a > b) {
109     return 1;
110   }
111   return 0;
112 }
113 
114 /**
115  * Removes leading and trailing whitespace from a string.
116  * @param {string} str
117  * @return {string} The trimmed string.
118  */
119 function trim(str) {
120   return str.replace(/^\s+|\s+$/g, "");
121 }
122 
123 //----------------------------------------------------------------
124 // string set
125 //----------------------------------------------------------------
126 
127 // TODO: unit-test the string set, or just switch it over to
128 // java.util.HashSet or something.
129 
130 /**
131  * Constructor for an object that holds a set of strings, with basic
132  * set operations.  This is better for keeping track of a set than
133  * using an associative arrays, because it avoids key collisions with
134  * javascript built-ins like 'toString', 'class', etc.
135  *
136  * @constructor
137  * @param {string} initialString1
138  * @param {string} initialString2
139  * @param {string} etc ...
140  * 
141  */
142 StringSet = function(initialElement1, initialElement2, etc) {
143   this._obj = {};
144   for (var i = 0; i < arguments.length; i++) {
145     if (typeof(arguments[i]) != 'string') {
146       throw new Error('StringSet constructor must take only string arguments.');
147     } else {
148       this._obj['$$'+arguments[i]] = true;
149     }
150   }
151 };
152 
153 /** @ignore */
154 StringSet.prototype.key = function(x) {
155   return '$$'+x;
156 }
157 
158 /**
159  * Returns whether this set contains the given string.
160  * 
161  * @param {string} x
162  * @return {boolean} Whether x exits in this set.
163  */
164 StringSet.prototype.contains = function(x) {
165   return (this._obj.hasOwnProperty(this.key(x)) &&
166 	  this._obj[this.key(x)] === true);
167 };
168 
169 /**
170  * Adds the given string to the set if it is not already in it.
171  * @param {string} x
172  */
173 StringSet.prototype.add = function(x) {
174   this._obj[this.key(x)] = true;
175 };
176 
177 /**
178  * Removes the given string from the set if it is contained in it.
179  * @param {string} x The string to remove from the set.
180  */
181 StringSet.prototype.remove = function(x) {
182   if (this._obj[this.key(x)]) {
183     delete this._obj[this.key(x)];
184   }
185 };
186 
187 /**
188  * Iterators over the strings in the set, calling the provided
189  * function on each element. 
190  * @param {function} f The function to call on each string in the
191  * set.
192  */
193 StringSet.prototype.forEach = function(f) {
194   var self = this;
195   eachProperty(this._obj, function(name) {
196     var realName = name.substring(2);
197     if (self.contains(realName)) f(realName);
198   });
199 }
200 
201 //----------------------------------------------------------------
202 // wget/wpost
203 //----------------------------------------------------------------
204 
205 /**
206  * The error thrown by wget and wpost if there's a problem retrieveing the requested URL.
207  *
208  * @constructor
209  * @param {string} message A status message describing why the action failed.
210  * @param {HttpResponse} info Additional info, including headers and received data, if any.
211  */
212 function HttpRequestError(message, info) {
213   this.message = message;
214   this.info = info;
215   this.name = "HttpRequestError";
216 }
217 HttpRequestError.prototype = new Error();
218 
219 /**
220  * @class Description of an object containing the properties of the HTTP response returned by <code>wget</code> and <code>wpost</code>
221  *
222  * @name HttpResponse
223  */
224 
225 /**
226  * The status code of the server's response, or an internal status code if connecting to the server failed.
227  * @name status
228  * @type number
229  * @memberOf HttpResponse
230  */
231 /**
232  * An explanation of the status code if it is an internal code (that is, less than 0)                                  
233  * @name statusInfo
234  * @type string
235  * @memberOf HttpResponse
236  */
237 /**
238  * The data returned by the server, if any.                                                                                  
239  * @name data
240  * @type string
241  * @memberOf HttpResponse
242  */
243 /**
244  * The content type returned by the server, if any.                                                                   
245  * @name contentType
246  * @type string
247  * @memberOf HttpResponse
248  */
249 /**
250  * An object containing property names and values corresponding to the headers returned by the server.                    
251  * @name headers
252  * @type object
253  * @memberOf HttpResponse
254  */
255 
256 function _paramObjectToParamArray(params, enc) {
257   var pa = [];
258   eachProperty(params, function(k, v) {
259     pa.push(enc ? encodeURIComponent(k.toString()) : k.toString());
260     pa.push(enc ? encodeURIComponent(v.toString()) : v.toString());
261   });
262   return pa;
263 }
264 
265 
266 /**
267  * Fetches the text of a URL and returns it as a string.
268  *
269  * @example
270 g = wget("google.com");
271 page.setMode("plain");
272 print(raw(g));
273  * 
274  * @param {string} url The name of the url to retreive. If the
275  * transport is not specified, HTTP is assumed.
276  * @param {object} [params] Optional parameters to include with the
277  * GET, as a dictionary of {name: value} entries.
278  * @param {object} [options] Optional object with three optional 
279  * properties:<ul>
280  * <li><strong>headers:</strong> HTTP request headers to send 
281  * with the GET, as a dictionary of {name: value} entries.</li>
282  * <li><strong>followRedirects:</strong> A boolean indicating
283  * whether to follow redirect headers returned by the server.
284  * Defaults to <code>true</code>.</li>
285  * <li><strong>complete:</strong> If <code>true</code>, wget returns
286  * an object containing all information about the response, not just
287  * its contents.  Otherwise, wget throws a HttpRequestError if it
288  * encounters an error GETing.
289  * </ul>
290  * @return {string} The full text of the url's content.
291  * @return {HttpResponse} The "complete" response, returned if the "complete" parameter is <code>true</code>.
292  */
293 function wget(url, params, options) {
294   if (!url.match(/^\w+\:\/\//)) {
295     url = "http://" + url;
296   } 
297   var pa = _paramObjectToParamArray(params, false);
298   var ha = _paramObjectToParamArray(options ? options.headers : undefined, false);
299   var followRedir = (options === true ? true : (options && options.followRedirects === false ? false : true));
300   var ret = appjet._native.simplehttpclient_get(url, pa, ha, followRedir);
301   if ((options === true) || (options && options.complete))
302     return ret;
303   if (ret.status >= 200 && ret.status < 300) {
304     return ret.data;
305   } else {
306     throw new HttpRequestError(ret.statusInfo || ret.status, ret);
307   }
308 }
309 
310 /**
311  * Simple way to POST data to a URL and get back the response.  Values of params will
312  * automatically be escaped.
313  *
314  * @example
315 result = wpost("example.com", {id: 25, value: "here is the post value"});
316  *
317  * @param {string} url The url to POST to.
318  * @param {object} [params] Optional parameters to include with the
319  * POST, as a dictionary of {name: value} entries.
320  * @param {object} [options] Optional object with three optional 
321  * properties:<ul>
322  * <li><strong>headers:</strong> HTTP request headers to send 
323  * with the POST, as a dictionary of {name: value} entries.</li>
324  * <li><strong>followRedirects:</strong> A boolean indicating
325  * whether to follow redirect headers returned by the server.</li>
326  * <li><strong>complete:</strong> If <code>true</code>, wpost returns
327  * an object containing all information about the response, not just
328  * its contents.  Otherwise, wpost throws a HttpRequestError if it
329  * encounters an error POSTing.
330  * </ul>
331  * @return {string} The full text returned from the server POSTed to.
332  * @return {HttpResponse} The complete response, returned if the "complete" parameter is <code>true</code>.
333  */
334 function wpost(url, params, options) {
335   if (!url.match(/^\w+\:\/\//)) {
336     url = "http://" + url;
337   }
338   var pa = _paramObjectToParamArray(params, true);
339   var ha = _paramObjectToParamArray(options ? options.headers : undefined, false);
340   var followRedir = (options === true ? true : (options && options.followRedirects === false ? false : true));
341   var ret = appjet._native.simplehttpclient_post(url, pa, ha, followRedir);
342   if ((options === true) || (options && options.complete))
343     return ret;
344   if (ret.status >= 200 && ret.status < 300) {
345     return ret.data;
346   } else {
347     throw new HttpRequestError(ret.statusInfo || ret.status, ret);
348   }
349 }
350 
351 /**
352  * Simple way to send an email to a single recipient. Emails will have a
353  * "from" address of <code>noreply@{appjet.appName}.{appjet.mainDomain}</code>.
354  *
355  * Sending is limited to 100 emails per developer account per day.  However,
356  * emails sent to the address on file for the app's owner are not counted
357  * toward this limit.
358  *
359  * @example
360 result = sendEmail("noone@example.com", "Test Subject", 
361                    "Greetings!", {"Reply-To": "sender@example.com"});
362  *
363  * @param {string} toAddress The one email address to send a message to.
364  * @param {string} subject The message subject.
365  * @param {string} body The message body.
366  * @param {object} [headers] Optional headers to include in the
367  * message, as a dictionary of {name: value} entries.
368  */
369 function sendEmail(toAddress, subject, body, headers) {
370   var pa = _paramObjectToParamArray(headers, false);
371   var ret = appjet._native.email_sendEmail(toAddress, subject, body, pa);
372   if (ret != "")
373     throw new Error(ret);
374 }
375 
376