Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Finished a decent first release.

  • Loading branch information...
commit 57539f7c10ce13d91341e483b2a1f1759c225bb3 1 parent b31ad18
@jacwright jacwright authored
View
0  README
No changes.
View
101 README.md
@@ -0,0 +1,101 @@
+wurfl.js
+========
+
+wurfl.js is a node.js library to make it easy to use wurfl. What is wurfl? From the site http://wurfl.sourceforge.net/:
+
+```
+WURFL is a Device Description Repository (DDR), i.e. a software component which contains the descriptions of thousands of mobile devices. In its simplest incarnation, WURFL is an XML configuration file plus a set of programming APIs to access the data in real-time environments.
+```
+
+Usage
+-----
+
+### Installation
+
+Install node.js (http://nodejs.org) first, then npm (http://npmjs.org), then:
+
+```bash
+npm install wurfl
+```
+
+### Usage
+
+Basic usage allows getting up and running quickly.
+
+```javascript
+var wurfl = require('wurfl');
+
+wurfl.loadSync();
+
+// wurfl xml file is finished loading into a memory-efficient structure
+
+var userAgent = 'Mozilla/4.0 (compatible; MSIE 4.01; Windows CE; O2 Xda 2s;PPC;240x320; PPC; 240x320)';
+var deviceInfo = wurfl.get(userAgent);
+console.log(deviceInfo.model_name); // "Xda IIs"
+```
+
+You may want to use your own (read "updated") wurfl.xml file. This is easy:
+
+```javascript
+var wurflFile = __dirname + '/wurfl.xml';
+wurfl.loadSync(wurflFile);
+```
+
+Loading and parsing a large xml file can be take awhile and be CPU-heavy. You may use load() to asynchronously load the xml file:
+
+```javascript
+wurfl.load(function() {
+ // ready to use, wurfl.get(userAgent);
+});
+```
+
+Or with your own xml file:
+
+```javascript
+wurfl.load(myWurflFile, function() {
+ // my own file
+});
+```
+
+When using your own file, it would be great to update the file without needing to restart the node.js server:
+
+```javascript
+wurfl.watch(myWurflFile);
+```
+
+When updating using watch it will always be asynchronous. The data structure will be updated automatically when the file is changed. You may also be notified when that happens if you want to know when it gets updated:
+
+```javascript
+wurfl.watch(myWurflFile, function() {
+ console.log('XML file updated');
+});
+```
+
+The most common use-case is to load the file up when your script starts synchronously since you need it to exist before you start querying against it, but then add a watch to asynchronously update it later:
+
+```javascript
+var wurfl = require('wurfl');
+var wurflFile = __dirname + '/wurfl.xml';
+
+wurfl.loadSync(wurflFile);
+wurfl.watch(myWurflFile);
+
+var userAgent = 'Mozilla/4.0 (compatible; MSIE 4.01; Windows CE; O2 Xda 2s;PPC;240x320; PPC; 240x320)';
+var deviceInfo = wurfl.get(userAgent);
+console.log(deviceInfo.model_name); // "Xda IIs"
+```
+
+That's all for the API:
+
+* wurfl.loadSync(optFileName)
+* wurfl.load(optFileName, optCallback)
+* wurfl.watch(optFileName, optCallback)
+* wurfl.get(userAgent)
+
+To see what properties are available in the info object you get back from wurfl.get() look at the wurfl.xml file. Each group is an object on info (e.g. info.css is a group) and each capability is a property on the group (e.g. info.css.css_spriting is a capability).
+
+### Optimizations
+
+The devices in the wurfl.xml file "inherit" from other devices, specified in the XML by the fall_back attribute. In order to optimize for memory and not duplicate the base properties over and over again for every single device, I've structured the groups to have their own class that extends the fall_back group class. I've set all the capabilites for a group on its prototype object. And for groups that weren't overridden, the device just points to its fall_back device's group. This way, when you access a group's capability it will pull it from the group's prototype object or from further up the prototype chain back to the top-level group instance.
+
+In addition to optimizing for memory I've also added a worker using node-webworkers to load and parse the XML file in a separate process from the main script. This allows the wurfl data to be loaded and/or updated without blocking a large chunk of time from your main script. This can be up to several seconds to load and parse the XML file. The worker is lazily created however, so if you only use loadSync() and get() it won't create the worker.
View
68 lib/parser.js
@@ -0,0 +1,68 @@
+var fs = require('fs');
+var path = require('path');
+var expat = require('node-expat');
+
+
+function watch(file, options, callback) {
+ if (typeof options === 'function') {
+ callback = options;
+ options = {};
+ }
+ options = options || {};
+
+ fs.watchFile(file, options, function(curr, prev) {
+ if (curr.mtime === prev.mtime) return;
+ load(file, callback);
+ });
+}
+
+
+function load(file, callback) {
+ if (typeof file === 'function') {
+ callback = file;
+ file = null;
+ }
+
+ fs.readFile(file, function(err, contents) {
+ if (err) return console.error('Could not load wurfl.xml file:', err);
+ var devices = parse(contents);
+ if (callback) callback(devices);
+ });
+}
+
+
+function parse(contents) {
+ var parser = new expat.Parser();
+
+ var devices = [];
+ var device;
+ var group;
+
+ parser.on('startElement', function(name, attr) {
+
+ if (name === 'device') {
+ device = attr;
+ device.groups = {};
+ devices.push(device);
+ } else if (name === 'group') {
+ device.groups[attr.id] = group = {};
+ } else if (name === 'capability') {
+ var value = attr.value;
+ if (parseFloat(value).toString() === value) {
+ value = parseFloat(value);
+ } else if (value === 'false') {
+ value = false;
+ } else if (value === 'true') {
+ value = true;
+ }
+ group[attr.name] = value;
+ }
+ });
+
+ parser.parse(contents);
+ return devices;
+}
+
+exports.watch = watch;
+exports.load = load;
+exports.parse = parse;
View
20 lib/worker.js
@@ -0,0 +1,20 @@
+var parser = require(__dirname + '/parser');
+
+
+onmessage = function(event) {
+ var data = event.data;
+ var method = data.method;
+ var file = data.file;
+ if (!method) throw new TypeError('Message does not contain method');
+ if (!file) throw new TypeError('Message does not contain file');
+ if (!parser.hasOwnProperty(method)) throw new ReferenceError('Method ' + method + ' is not valid');
+
+ parser[method](file, function(devices) {
+ if (!devices) return;
+ postMessage({
+ method: method,
+ file: file,
+ devices: devices
+ });
+ });
+};
View
156 lib/wurfl.js
@@ -0,0 +1,156 @@
+var fs = require('fs');
+var path = require('path');
+var expat = require('node-expat');
+var parser = require('./parser');
+var Worker = require('webworker').Worker;
+
+
+var wurflFile = path.join(__dirname, '../wurfl.xml');
+var userAgents = {};
+var worker;
+var callbacks = {watch: {}, load: {}};
+
+
+function get(userAgent) {
+ return userAgents[userAgent];
+}
+
+function close() {
+ if (worker) worker.terminate();
+}
+
+// lazily load the worker
+function getWorker() {
+ if (!worker) {
+ worker = new Worker(path.join(__dirname, './worker.js'));
+
+ worker.onmessage = function(event) {
+ var data = event.data;
+ var method = data.method;
+ var file = data.file;
+ var devices = data.devices;
+ packageDevices(devices);
+ var callback = callbacks[method][file];
+ if (callback) callback();
+ };
+
+ worker.onerror = function(error) {
+ console.error(error);
+ }
+ }
+
+ return worker;
+}
+
+
+function watch(file, callback) {
+ if (file && !options && typeof file !== 'string') {
+ options = file;
+ file = undefined;
+ }
+ file = file || wurflFile;
+
+ if (callback) callbacks.watch[file] = callback;
+
+ getWorker().postMessage({
+ method: 'watch',
+ file: file
+ });
+}
+
+
+function load(file, callback) {
+ if (typeof file === 'function') {
+ callback = file;
+ file = undefined;
+ }
+ file = file || wurflFile;
+
+ if (callback) callbacks.load[file] = callback;
+
+ getWorker().postMessage({
+ method: 'load',
+ file: file
+ });
+}
+
+
+function loadSync(file) {
+ file = file || wurflFile;
+
+ var contents = fs.readFileSync(file);
+ packageDevices(parser.parse(contents));
+}
+
+
+function packageDevices(devices) {
+ var agents = {};
+
+ // add the lookup first
+ devices.forEach(function(device) {
+ devices[device.id] = device;
+ });
+
+ // create device objects from each one
+ devices.forEach(function(device, i, array) {
+ if (device instanceof Device) return;
+ array[i] = createDevice(device, devices, agents);
+ });
+
+ userAgents = agents;
+}
+
+
+function createDevice(attr, lookup, agents) {
+ if (attr.fall_back !== 'root' && !(lookup[attr.fall_back] instanceof Device)) {
+ createDevice(lookup[attr.fall_back], lookup, agents);
+ }
+
+ var groups = attr.groups;
+ delete attr.groups;
+ var device = new Device(attr);
+ lookup[device.id] = device; // add a lookup by id
+ agents[device.user_agent] = device; // and a lookup by user agent
+
+ var parent = lookup[attr.fall_back];
+ if (parent) {
+ for (var i in parent) {
+ var group = parent[i];
+ if (group instanceof Group) {
+ device[i] = group;
+ }
+ }
+ }
+
+ for (var id in groups) {
+ group = device[id];
+ var GroupClass = function() {};
+ GroupClass.prototype = group ? new group.constructor() : new Group();
+ var groupProto = GroupClass.prototype;
+ groupProto.constructor = GroupClass;
+ groupProto.id = device.id + '.' + id;
+ device[id] = group = new GroupClass();
+
+ var capabilities = groups[id];
+ for (var name in capabilities) {
+ groupProto[name] = capabilities[name];
+ }
+ }
+
+ return device;
+}
+
+
+function Device(attr) {
+ for (var i in attr)
+ if (attr.hasOwnProperty(i)) this[i] = attr[i];
+}
+
+function Group() {}
+
+
+exports.watch = watch;
+exports.load = load;
+exports.loadSync = loadSync;
+exports.get = get;
+exports.close = close;
View
19 package.json
@@ -0,0 +1,19 @@
+{
+ "name": "wurfl",
+ "author": "Jacob Wright <jacwright@gmail.com> (jacwright.com)",
+ "version": "0.1.0",
+ "main": "./lib/wurfl"
+ "description": "NodeJS library for loading Wireless Universal Resource File (wurfl) xml file efficiently into memory for use in applications that need to lookup mobile device capabilities by their user agent."
+ "scripts": {
+ "test": "node test/test.js"
+ },
+ "dependencies": {
+ "node-expat": "1.4.0",
+ "webworker": "0.8.4"
+ },
+ "homepage": "https://github.com/touchads/node-wurfl-api"
+ "repository": {
+ "type": "git",
+ "url": "git://github.com/touchads/node-wurfl-api.git"
+ }
+}
View
36 test/test.js
@@ -0,0 +1,36 @@
+var wurfl = require('../lib/wurfl');
+
+console.log('Loading and parsing wurfl.xml');
+wurfl.loadSync();
+
+sanityCheck();
+
+console.log('Everything looks good for synchronous loading');
+
+
+console.log('Loading via worker');
+wurfl.load(function() {
+ sanityCheck();
+
+ console.log('Everything looks good for asynchronous loading too');
+
+ wurfl.close();
+});
+
+
+// super simple testing just for sanity check
+function sanityCheck() {
+ var info = wurfl.get('Mozilla/4.0 (compatible; MSIE 4.01; Windows CE; O2 Xda 2s;PPC;240x320; PPC; 240x320)');
+
+ if (!info) {
+ throw new Error('Loading by user agent not working');
+ }
+
+ if (info.playback.playback_mp4 !== true) {
+ throw new Error('Specific info not working');
+ }
+
+ if (info.product_info.pointing_method !== 'stylus') {
+ throw new Error('Inherited info not working');
+ }
+}
View
389,043 wurfl.xml
389,043 additions, 0 deletions not shown
Please sign in to comment.
Something went wrong with that request. Please try again.