diff --git a/lib/OpenLayers/Format.js b/lib/OpenLayers/Format.js index c809c46f92..0faf7170ec 100644 --- a/lib/OpenLayers/Format.js +++ b/lib/OpenLayers/Format.js @@ -13,6 +13,12 @@ */ OpenLayers.Format = OpenLayers.Class({ + /** + * Property: options + * {Object} A reference to options passed to the constructor. + */ + options: null, + /** * APIProperty: externalProjection * {} When passed a externalProjection and @@ -54,6 +60,14 @@ OpenLayers.Format = OpenLayers.Class({ */ initialize: function(options) { OpenLayers.Util.extend(this, options); + this.options = options; + }, + + /** + * APIMethod: destroy + * Clean up. + */ + destroy: function() { }, /** diff --git a/lib/OpenLayers/Format/XML.js b/lib/OpenLayers/Format/XML.js index a55f120ff6..95965ab516 100644 --- a/lib/OpenLayers/Format/XML.js +++ b/lib/OpenLayers/Format/XML.js @@ -19,6 +19,37 @@ */ OpenLayers.Format.XML = OpenLayers.Class(OpenLayers.Format, { + /** + * Property: namespaces + * {Object} Mapping of namespace aliases to namespace URIs. Properties + * of this object should not be set individually. + */ + namespaces: {}, + + /** + * Property: defaultNamespace + * {String} The default namespace alias for creating element nodes. + */ + defaultNamespace: null, + + /** + * Property: readers + * Contains public functions, grouped by namespace prefix, that will + * be applied when a namespaced node is found matching the function + * name. The function will be applied in the scope of this parser + * with two arguments: the node being read and a context object passed + * from the parent. + */ + readers: {}, + + /** + * Property: writers + * As a compliment to the property, this structure contains public + * writing functions grouped by namespace alias and named like the + * node names they produce. + */ + writers: {}, + /** * Property: xmldom * {XMLDom} If this browser uses ActiveX, this will be set to a XMLDOM @@ -45,6 +76,15 @@ OpenLayers.Format.XML = OpenLayers.Class(OpenLayers.Format, { } OpenLayers.Format.prototype.initialize.apply(this, [options]); }, + + /** + * APIMethod: destroy + * Clean up. + */ + destroy: function() { + this.xmldom = null; + OpenLayers.Format.prototype.destroy.apply(this, arguments); + }, /** * APIMethod: read @@ -384,6 +424,196 @@ OpenLayers.Format.XML = OpenLayers.Class(OpenLayers.Format, { } }, + /** + * Method: getNamespacePrefix + * Get the namespace prefix for a given uri from the object. + * + * Returns: + * {String} A namespace prefix or null if none found. + */ + getNamespacePrefix: function(uri) { + var prefix = null; + if(uri == null) { + prefix = this.defaultPrefix; + } else { + var prefix = null; + for(var p in this.namespaces) { + if(this.namespaces[p] == uri) { + prefix = p; + break; + } + } + } + return prefix; + }, + + /** + * Method: createElementNSPlus + * Shorthand for creating namespaced elements with optional attributes and + * child text nodes. + * + * Parameters: + * name - {String} The qualified node name. + * options - {Object} Optional object for node configuration. + * + * Valid options: + * uri - {String} Optional namespace uri for the element - supply a prefix + * instead if the namespace uri is a property of the format's namespace + * object. + * attributes - {Object} Optional attributes to be set using the + * method. + * value - {String} Optional text to be appended as a text node. + * + * Returns: + * {Element} An element node. + */ + createElementNSPlus: function(name, options) { + options = options || {}; + var loc = name.indexOf(":"); + // order of prefix preference + // 1. in the uri option + // 2. in the prefix option + // 3. in the qualified name + // 4. from the defaultPrefix + var uri = options.uri || this.namespaces[options.prefix]; + if(!uri) { + loc = name.indexOf(":"); + uri = this.namespaces[name.substring(0, loc)]; + } + if(!uri) { + uri = this.namespaces[this.defaultPrefix]; + } + var node = this.createElementNS(uri, name); + if(options.attributes) { + this.setAttributes(node, options.attributes); + } + if(options.value) { + node.appendChild(this.createTextNode(options.value)); + } + return node; + }, + + /** + * Method: setAttributes + * Set multiple attributes given key value pairs from an object. + * + * Parameters: + * node - {Element} An element node. + * obj - {Object || Array} An object whose properties represent attribute + * names and values represent attribute values. If an attribute name + * is a qualified name ("prefix:local"), the prefix will be looked up + * in the parsers {namespaces} object. If the prefix is found, + * setAttributeNS will be used instead of setAttribute. + */ + setAttributes: function(node, obj) { + var value, loc, alias, uri; + for(var name in obj) { + if(obj[name] != null && obj[name].toString) { + value = obj[name].toString(); + // check for qualified attribute name ("prefix:local") + uri = this.namespaces[name.substring(0, name.indexOf(":"))] || null; + this.setAttributeNS(node, uri, name, value); + } + } + }, + + /** + * Method: readNode + * Shorthand for applying one of the named readers given the node + * namespace and local name. Readers take two args (node, obj) and + * generally extend or modify the second. + * + * Parameters: + * node - {DOMElement} The node to be read (required). + * obj - {Object} The object to be modified (optional). + * + * Returns: + * {Object} The input object, modified (or a new one if none was provided). + */ + readNode: function(node, obj) { + if(!obj) { + obj = {}; + } + var prefix = this.getNamespacePrefix(node.namespaceURI); + var local = node.nodeName.split(":").pop(); + var group = this.readers[prefix]; + if(group) { + var reader = group[local] || group["*"]; + if(reader) { + reader.apply(this, [node, obj]); + } + } + return obj; + }, + + /** + * Method: readChildNodes + * Shorthand for applying the named readers to all children of a node. + * For each child of type 1 (element), is called. + * + * Parameters: + * node - {DOMElement} The node to be read (required). + * obj - {Object} The object to be modified (optional). + * + * Returns: + * {Object} The input object, modified. + */ + readChildNodes: function(node, obj) { + if(!obj) { + obj = {}; + } + var children = node.childNodes; + var child; + for(var i=0; i group. If a local name is used (e.g. "Name") then + * the namespace of the parent is assumed. If a local name is used + * and no parent is supplied, then the default namespace is assumed. + * obj - {Object} Structure containing data for the writer. + * parent - {DOMElement} Result will be appended to this node. If no parent + * is supplied, the node will not be appended to anything. + * + * Returns: + * {DOMElement} The child node. + */ + writeNode: function(name, obj, parent) { + var prefix, local; + var split = name.indexOf(":"); + if(split > 0) { + prefix = name.substring(0, split); + local = name.substring(split + 1); + } else { + if(parent) { + prefix = this.getNamespacePrefix(parent.namespaceURI); + } else { + prefix = this.defaultPrefix; + } + local = name; + } + var child = this.writers[prefix][local].apply(this, [obj]); + if(parent) { + parent.appendChild(child); + } + return child; + }, + CLASS_NAME: "OpenLayers.Format.XML" }); diff --git a/tests/Format.html b/tests/Format.html index 70a1d5fe6a..9c761dff28 100644 --- a/tests/Format.html +++ b/tests/Format.html @@ -4,7 +4,7 @@ diff --git a/tests/Format/XML.html b/tests/Format/XML.html index aff544e134..c9d3b9e3b1 100644 --- a/tests/Format/XML.html +++ b/tests/Format/XML.html @@ -27,7 +27,7 @@ '<' + '/ol:root>'; function test_Format_XML_constructor(t) { - t.plan(5); + t.plan(13); var options = {'foo': 'bar'}; var format = new OpenLayers.Format.XML(options); @@ -38,6 +38,36 @@ t.eq(typeof format.write, "function", "format has a write function"); t.ok(!window.ActiveXObject || format.xmldom, "browsers with activeX must have xmldom"); + + // test namespaces + t.ok(format.namespaces instanceof Object, "format has namespace object"); + var namespaces = {"foo": "bar"}; + format = new OpenLayers.Format.XML({namespaces: namespaces}); + t.eq(format.namespaces, namespaces, "format.namespaces correctly set in constructor"); + + // test default prefix + t.eq(format.defaultPrefix, null, "defaultPrefix is null by default"); + format = new OpenLayers.Format.XML({defaultPrefix: "foo"}); + t.eq(format.defaultPrefix, "foo", "defaultPrefix correctly set in constructor"); + + // test readers + t.ok(format.readers instanceof Object, "format has readers object"); + var readers = {"foo": "bar"}; + format = new OpenLayers.Format.XML({readers: readers}); + t.eq(format.readers, readers, "format.readers correctly set in constructor"); + + // test readers + t.ok(format.writers instanceof Object, "format has writers object"); + var writers = {"foo": "bar"}; + format = new OpenLayers.Format.XML({writers: writers}); + t.eq(format.writers, writers, "format.writers correctly set in constructor"); + } + + function test_destroy(t) { + t.plan(1); + var format = new OpenLayers.Format.XML(); + format.destroy(); + t.eq(format.xmldom, null, "xmldom set to null for all browsers"); } function test_Format_XML_read(t) { @@ -260,6 +290,359 @@ found = format.hasAttributeNS(nodes[0], taUri, "nothing"); t.ok(found === false, "returns false for bad attribute"); } + + function test_getNamespacePrefix(t) { + t.plan(6); + + // test that getNamespacePrefix returns null with no ns defined + var format = new OpenLayers.Format.XML(); + var got = format.getNamespacePrefix("http://example.com/foo"); + t.eq(got, null, "returns null when no namespaces are defined"); + + format.defaultPrefix = "def"; + format.namespaces = { + "def": "http://example.com/default", + "foo": "http://example.com/foo", + "bar": "http://example.com/bar" + }; + + var cases = [ + {uri: null, expect: "def"}, + {uri: "http://example.com/default", expect: "def"}, + {uri: "http://example.com/foo", expect: "foo"}, + {uri: "http://example.com/bar", expect: "bar"}, + {uri: "http://example.com/nothing", expect: null} + ]; + + var test; + for(var i=0; i" + + "" + + "" + + "-180" + + "90" + + "" + + "some text for first marker" + + "" + + "" + + "" + + "" + + "180" + + "-90" + + "" + + "some text for second marker" + + "" + + "" + + ""; + + var expect = [ + new OpenLayers.Feature.Vector( + new OpenLayers.Geometry.Point(-180, 90), + { + name: 'my marker 1', + link: 'http://host/path/1', + detail: 'some text for first marker' + } + ), + new OpenLayers.Feature.Vector( + new OpenLayers.Geometry.Point(180, -90), + { + name: 'my marker 2', + link: 'http://host/path/2', + detail: 'some text for second marker' + } + ) + ]; + + var format = new OpenLayers.Format.XML({ + defaultPrefix: "foo", + namespaces: { + "foo": "http://example.com/foo", + "atom": "http://www.w3.org/2005/Atom" + }, + readers: { + "foo": { + "container": function(node, obj) { + var list = []; + this.readChildNodes(node, list); + obj.list = list; + }, + "marker": function(node, list) { + var feature = new OpenLayers.Feature.Vector(); + feature.attributes.name = node.getAttribute("name"); + this.readChildNodes(node, feature); + list.push(feature); + }, + "position": function(node, feature) { + var obj = {}; + this.readChildNodes(node, obj); + feature.geometry = new OpenLayers.Geometry.Point(obj.x, obj.y); + }, + "lon": function(node, obj) { + obj.x = this.getChildValue(node); + }, + "lat": function(node, obj) { + obj.y = this.getChildValue(node); + }, + "detail": function(node, feature) { + feature.attributes.detail = this.getChildValue(node); + } + }, + "atom": { + "link": function(node, feature) { + feature.attributes.link = node.getAttribute("href"); + } + } + } + }); + + // convert text to document node + var doc = format.read(text); + // read child nodes to get back some object + var obj = format.readChildNodes(doc); + // start comparing what we got to what we expect + var got = obj.list; + + t.plan(11); + t.eq(got.length, expect.length, "correct number of items parsed"); + t.eq(got[0].geometry.x, expect[0].geometry.x, "correct x coord parsed for marker 1"); + t.eq(got[0].geometry.y, expect[0].geometry.y, "correct y coord parsed for marker 1"); + t.eq(got[0].attributes.name, expect[0].attributes.name, "correct name parsed for marker 1"); + t.eq(got[0].attributes.detail, expect[0].attributes.detail, "correct detail parsed for marker 1"); + t.eq(got[0].attributes.link, expect[0].attributes.link, "correct link parsed for marker 1"); + t.eq(got[1].geometry.x, expect[1].geometry.x, "correct x coord parsed for marker 2"); + t.eq(got[1].geometry.y, expect[1].geometry.y, "correct y coord parsed for marker 2"); + t.eq(got[1].attributes.name, expect[1].attributes.name, "correct name parsed for marker 2"); + t.eq(got[1].attributes.detail, expect[1].attributes.detail, "correct detail parsed for marker 2"); + t.eq(got[1].attributes.link, expect[1].attributes.link, "correct link parsed for marker 2"); + + } + + function test_writeNode(t) { + + var features = [ + new OpenLayers.Feature.Vector( + new OpenLayers.Geometry.Point(-180, 90), + { + name: 'my marker 1', + link: 'http://host/path/1', + detail: 'some text for first marker' + } + ), + new OpenLayers.Feature.Vector( + new OpenLayers.Geometry.Point(180, -90), + { + name: 'my marker 2', + link: 'http://host/path/2', + detail: 'some text for second marker' + } + ) + ]; + + var expect = "" + + "" + + "" + + "" + + "-180" + + "90" + + "" + + "some text for first marker" + + "" + + "" + + "" + + "" + + "180" + + "-90" + + "" + + "some text for second marker" + + "" + + "" + + ""; + + var format = new OpenLayers.Format.XML({ + defaultPrefix: "foo", + namespaces: { + "foo": "http://example.com/foo", + "atom": "http://www.w3.org/2005/Atom" + }, + writers: { + "foo": { + "container": function(features) { + var node = this.createElementNSPlus("container"); + var feature; + for(var i=0; i" + }, { + description: "def prefixed name with default options", + node: format.createElementNSPlus("def:FooNode"), + expect: "" + }, { + description: "foo prefixed name with default options", + node: format.createElementNSPlus("foo:FooNode"), + expect: "" + }, { + description: "unprefixed name with uri option", + node: format.createElementNSPlus("FooNode", { + uri: "http://example.com/elsewhere" + }), + expect: "" + }, { + description: "foo prefixed name with uri option (overriding format.namespaces)", + node: format.createElementNSPlus("foo:FooNode", { + uri: "http://example.com/elsewhere" + }), + expect: "" + }, { + description: "foo prefixed name with attributes option", + node: format.createElementNSPlus("foo:FooNode", { + attributes: { + "id": "123", + "foo:attr1": "namespaced attribute 1", + "bar:attr2": "namespaced attribute 2" + } + }), + expect: "" + }, { + description: "foo prefixed name with attributes and value options", + node: format.createElementNSPlus("foo:FooNode", { + attributes: {"id": "123"}, + value: "text value" + }), + expect: "text value<" + "/foo:FooNode>" + } + ]; + + t.plan(cases.length); + var test; + for(var i=0; i" + }, { + description: "foo prefixed attribute", + node: format.createElementNSPlus("foo:Node"), + attributes: {"foo:id": "123"}, + expect: "" + }, { + description: "foo prefixed attribute with def prefixed node", + node: format.createElementNSPlus("def:Node"), + attributes: {"foo:id": "123"}, + expect: "" + }, { + description: "multiple attributes", + node: format.createElementNSPlus("def:Node"), + attributes: {"id": "123", "foo": "bar"}, + expect: "" + } + ]; + + t.plan(cases.length); + var test; + for(var i=0; i