From b3b9add06e1dfe4ed8e51e057ed8e5537c2f1b24 Mon Sep 17 00:00:00 2001 From: ahocevar Date: Fri, 6 Dec 2013 10:57:58 +0100 Subject: [PATCH] Add Insert, Update and Delete writers This change also adds some type annotations for better type checking, introduces different write options for writing transactions and queries, and provides new writeGetFeature and writeTransaction methods. --- src/objectliterals.jsdoc | 26 ++ src/ol/parser/ogc/wfsparser_v1.js | 235 +++++++++++++++--- test/spec/ol/parser/ogc/wfs_v1.test.js | 82 ++++-- .../ol/parser/ogc/xml/wfs_v1/Transaction.xml | 2 +- .../ogc/xml/wfs_v1/TransactionMulti.xml | 45 ++++ 5 files changed, 324 insertions(+), 66 deletions(-) create mode 100644 test/spec/ol/parser/ogc/xml/wfs_v1/TransactionMulti.xml diff --git a/src/objectliterals.jsdoc b/src/objectliterals.jsdoc index 83e0e9a6e1..ba449c5349 100644 --- a/src/objectliterals.jsdoc +++ b/src/objectliterals.jsdoc @@ -595,6 +595,32 @@ * calculations. */ +/** + * @typedef {Object} ol.parser.WFSWriteGetFeatureOptions + * @property {string} featureNS The namespace URI used for features. + * @property {string} featurePrefix The prefix for the feature namespace. + * @property {Array.} featureTypes The feature type names. + * @property {string|undefined} srsName SRS name. No srsName attribute will be + * set on geometries when this is not provided. + * @property {string|undefined} handle Handle. + * @property {string|undefined} outputFormat Output format. + * @property {Array.} nativeElements Native elements. Currently not + * supported. + * @property {number} maxFeatures Maximum number of features to fetch. + */ + +/** + * @typedef {Object} ol.parser.WFSWriteTransactionOptions + * @property {string} featureNS The namespace URI used for features. + * @property {string} featurePrefix The prefix for the feature namespace. + * @property {string} featureType The feature type name. + * @property {string|undefined} srsName SRS name. No srsName attribute will be + * set on geometries when this is not provided. + * @property {string|undefined} handle Handle. + * @property {Array.} nativeElements Native elements. Currently not + * supported. + */ + /** * @typedef {Object} ol.source.BingMapsOptions * @property {string|undefined} culture Culture code. Default is `en-us`. diff --git a/src/ol/parser/ogc/wfsparser_v1.js b/src/ol/parser/ogc/wfsparser_v1.js index 2fb5f685fa..4fae1ef137 100644 --- a/src/ol/parser/ogc/wfsparser_v1.js +++ b/src/ol/parser/ogc/wfsparser_v1.js @@ -1,31 +1,22 @@ goog.provide('ol.parser.ogc.WFS_v1'); +goog.require('goog.asserts'); goog.require('goog.dom.xml'); +goog.require('ol.expr.Call'); +goog.require('ol.expr.Identifier'); +goog.require('ol.expr.Literal'); +goog.require('ol.geom.Geometry'); goog.require('ol.parser.XML'); -/** - * @typedef {{featureNS: string, - featurePrefix: string, - featureTypes: Array., - handle: string, - outputFormat: string, - nativeElements: Array.<{ - vendorId: string, - safeToIgnore: boolean, - value: string - }>, - maxFeatures: number}} - */ -ol.parser.WFSWriteOptions; - - /** * @constructor * @extends {ol.parser.XML} + * @param {Object=} opt_options Options which will be set on this object. */ -ol.parser.ogc.WFS_v1 = function() { +ol.parser.ogc.WFS_v1 = function(opt_options) { this.defaultNamespaceURI = 'http://www.opengis.net/wfs'; + // TODO set errorProperty this.readers = {}; this.readers[this.defaultNamespaceURI] = { @@ -36,8 +27,13 @@ ol.parser.ogc.WFS_v1 = function() { }; this.writers = {}; this.writers[this.defaultNamespaceURI] = { + /** + * @param {ol.parser.WFSWriteGetFeatureOptions} options Options. + * @return {{node: Node, + * options: ol.parser.WFSWriteGetFeatureOptions}} Object. + * @this {ol.parser.XML} + */ 'GetFeature': function(options) { - options = /** @type {ol.parser.WFSWriteOptions} */(options); var node = this.createElementNS('wfs:GetFeature'); node.setAttribute('service', 'WFS'); node.setAttribute('version', this.version); @@ -61,29 +57,41 @@ ol.parser.ogc.WFS_v1 = function() { 'xsi:schemaLocation', this.schemaLocation); return {node: node, options: options}; }, + /** + * @param {{inserts: Array., + * updates: Array., + * deletes: Array., + * options: ol.parser.WFSWriteTransactionOptions}} obj Object. + * @return {Element} Node. + * @this {ol.parser.XML} + */ 'Transaction': function(obj) { - obj = obj || {}; - var options = /** {ol.parser.WFSWriteOptions} */(obj.options || {}); + var options = obj.options; + this.setFeatureType(options.featureType); + this.setFeatureNS(options.featureNS); + if (goog.isDef(options.srsName)) { + this.setSrsName(options.srsName); + } var node = this.createElementNS('wfs:Transaction'); node.setAttribute('service', 'WFS'); node.setAttribute('version', this.version); if (goog.isDef(options.handle)) { node.setAttribute('handle', options.handle); } - var i, ii; - var features = obj.features; - if (goog.isDefAndNotNull(features)) { - // TODO implement multi option for geometry types - var name, feature; - for (i = 0, ii = features.length; i < ii; ++i) { - feature = features[i]; - // TODO Update (use feature.getOriginal()) - // TODO Insert and Delete - if (goog.isDef(name)) { - this.writeNode(name, { - feature: feature, - options: options - }, null, node); + var i, ii, features, feature; + var operations = { + 'Insert': obj.inserts, + 'Update': obj.updates, + 'Delete': obj.deletes + }; + for (var name in operations) { + features = operations[name]; + if (!goog.isNull(features)) { + // TODO implement multi option for geometry types + for (i = 0, ii = features.length; i < ii; ++i) { + feature = features[i]; + this.writeNode(name, {feature: feature, options: options}, null, + node); } } } @@ -94,12 +102,147 @@ ol.parser.ogc.WFS_v1 = function() { } return node; }, + /** + * @param {{vendorId: string, safeToIgnore: boolean, value: string}} + * nativeElement Native element. + * @return {Node} Node. + * @this {ol.parser.XML} + */ 'Native': function(nativeElement) { var node = this.createElementNS('wfs:Native'); node.setAttribute('vendorId', nativeElement.vendorId); node.setAttribute('safeToIgnore', nativeElement.safeToIgnore); node.appendChild(this.createTextNode(nativeElement.value)); return node; + }, + /** + * @param {{feature: ol.Feature, + * options: ol.parser.WFSWriteTransactionOptions}} obj Object. + * @return {Element} Node. + * @this {ol.parser.XML} + */ + 'Insert': function(obj) { + var feature = obj.feature; + var options = obj.options; + var node = this.createElementNS('wfs:Insert'); + if (goog.isDef(options) && goog.isDef(options.handle)) { + this.setAttributeNS(node, this.defaultNamespaceURI, 'handle', + options.handle); + } + if (goog.isDef(options.srsName)) { + this.setSrsName(options.srsName); + } + this.writeNode('_typeName', feature, options.featureNS, node); + return node; + }, + /** + * @param {{feature: ol.Feature, + * options: ol.parser.WFSWriteTransactionOptions}} obj Object. + * @return {Element} Node. + * @this {ol.parser.XML} + */ + 'Update': function(obj) { + var feature = obj.feature; + var options = obj.options; + var node = this.createElementNS('wfs:Update'); + this.setAttributeNS(node, this.defaultNamespaceURI, 'typeName', + (goog.isDef(options.featureNS) ? options.featurePrefix + ':' : '') + + options.featureType); + if (goog.isDef(options.handle)) { + this.setAttributeNS(node, this.defaultNamespaceURI, 'handle', + options.handle); + } + if (goog.isDef(options.featureNS)) { + node.setAttribute('xmlns:' + options.featurePrefix, options.featureNS); + } + + // add in fields + var original = feature.getOriginal(); + var originalAttributes = goog.isNull(original) ? + undefined : original.getAttributes(); + var attributes = feature.getAttributes(); + var attribute; + for (var key in attributes) { + attribute = attributes[key]; + // TODO Only add geometries whose values have changed + if (goog.isDef(attribute) && (attribute instanceof ol.geom.Geometry || + (!goog.isDef(originalAttributes) || + attribute != originalAttributes[key]))) { + this.writeNode('Property', {name: key, value: attribute}, null, node); + } + } + + // add feature id filter + var fid = feature.getId(); + goog.asserts.assert(goog.isDef(fid)); + this.writeNode('Filter', new ol.expr.Call(new ol.expr.Identifier( + ol.expr.functions.FID), [new ol.expr.Literal(fid)]), + 'http://www.opengis.net/ogc', node); + + return node; + }, + 'Property': function(obj) { + var node = this.createElementNS('wfs:Property'); + this.writeNode('Name', obj.name, null, node); + if (!goog.isNull(obj.value)) { + this.writeNode('Value', obj.value, null, node); + } + return node; + }, + /** + * @param {string} name Name. + * @return {Element} Node. + * @this {ol.parser.XML} + */ + 'Name': function(name) { + var node = this.createElementNS('wfs:Name'); + node.appendChild(this.createTextNode(name)); + return node; + }, + /** + * @param {string|ol.geom.Geometry} obj Object. + * @return {Element} Node. + * @this {ol.parser.XML} + */ + 'Value': function(obj) { + var node; + if (obj instanceof ol.geom.Geometry) { + node = this.createElementNS('wfs:Value'); + node.appendChild( + this.getFilterParser().getGmlParser().writeGeometry(obj)); + } else { + goog.asserts.assertString(obj); + node = this.createElementNS('wfs:Value'); + node.appendChild(this.createTextNode(obj)); + } + return node; + }, + /** + * @param {{feature: ol.Feature, + * options: ol.parser.WFSWriteTransactionOptions}} obj Object. + * @return {Element} Node. + * @this {ol.parser.ogc.GML} + */ + 'Delete': function(obj) { + var feature = obj.feature; + var options = obj.options; + var node = this.createElementNS('wfs:Delete'); + this.setAttributeNS(node, this.defaultNamespaceURI, 'typeName', + (goog.isDef(options.featureNS) ? options.featurePrefix + ':' : '') + + options.featureType); + if (goog.isDef(options.handle)) { + this.setAttributeNS(node, options.defaultNamespaceURI, 'handle', + options.handle); + } + if (goog.isDef(options.featureNS)) { + node.setAttribute('xmlns:' + options.featurePrefix, options.featureNS); + } + var fid = feature.getId(); + goog.asserts.assert(goog.isDef(fid)); + this.writeNode('Filter', new ol.expr.Call(new ol.expr.Identifier( + ol.expr.functions.FID), [new ol.expr.Literal(fid)]), + 'http://www.opengis.net/ogc', node); + return node; } }; goog.base(this); @@ -204,13 +347,27 @@ ol.parser.ogc.WFS_v1.prototype.read = function(data) { /** - * @param {Array.} features The features to write out. - * @param {ol.parser.WFSWriteOptions} options Write options. + * @param {ol.parser.WFSWriteGetFeatureOptions} options Options. + * @return {string} A serialized WFS GetFeature query. + */ +ol.parser.ogc.WFS_v1.prototype.writeGetFeature = function(options) { + var root = this.writers[this.defaultNamespaceURI]['GetFeature'] + .call(this, options); + return this.serialize(root); +}; + + +/** + * @param {Array.} inserts The features to insert. + * @param {Array.} updates The features to update. + * @param {Array.} deletes The features to delete. + * @param {ol.parser.WFSWriteTransactionOptions} options Write options. * @return {string} A serialized WFS transaction. */ -ol.parser.ogc.WFS_v1.prototype.write = function(features, options) { - var root = this.writeNode('Transaction', {features: features, - options: options}); +ol.parser.ogc.WFS_v1.prototype.writeTransaction = + function(inserts, updates, deletes, options) { + var root = this.writeNode('Transaction', {inserts: inserts, + updates: updates, deletes: deletes, options: options}); this.setAttributeNS( root, 'http://www.w3.org/2001/XMLSchema-instance', 'xsi:schemaLocation', this.schemaLocation); diff --git a/test/spec/ol/parser/ogc/wfs_v1.test.js b/test/spec/ol/parser/ogc/wfs_v1.test.js index 22bf07fc01..69fc103d81 100644 --- a/test/spec/ol/parser/ogc/wfs_v1.test.js +++ b/test/spec/ol/parser/ogc/wfs_v1.test.js @@ -19,17 +19,15 @@ describe('ol.parser.ogc.WFS', function() { var url = 'spec/ol/parser/ogc/xml/wfs_v1/GetFeature.xml'; afterLoadXml(url, function(xml) { var p = new ol.parser.ogc.WFS_v1_0_0(); - var output = p.writers[p.defaultNamespaceURI]['GetFeature']. - apply(p, [{ - featureNS: 'http://www.openplans.org/topp', - featureTypes: ['states'], - featurePrefix: 'topp', - handle: 'handle_g', - maxFeatures: 1, - outputFormat: 'json' - } - ]); - expect(goog.dom.xml.loadXml(p.serialize(output))).to.xmleql(xml); + var output = p.writeGetFeature({ + featureNS: 'http://www.openplans.org/topp', + featureTypes: ['states'], + featurePrefix: 'topp', + handle: 'handle_g', + maxFeatures: 1, + outputFormat: 'json' + }); + expect(goog.dom.xml.loadXml(output)).to.xmleql(xml); done(); }); }); @@ -38,12 +36,44 @@ describe('ol.parser.ogc.WFS', function() { var url = 'spec/ol/parser/ogc/xml/wfs_v1/Transaction.xml'; afterLoadXml(url, function(xml) { var p = new ol.parser.ogc.WFS_v1_0_0(); - var output = p.writers[p.defaultNamespaceURI]['Transaction']. - apply(p, [{ - options: {handle: 'handle_t'} - } - ]); - expect(goog.dom.xml.loadXml(p.serialize(output))).to.xmleql(xml); + var output = p.writeTransaction(null, null, null, {handle: 'handle_t'}); + expect(goog.dom.xml.loadXml(output)).to.xmleql(xml); + done(); + }); + }); + + it('handles writing out transactions', function(done) { + var url = 'spec/ol/parser/ogc/xml/wfs_v1/TransactionMulti.xml'; + afterLoadXml(url, function(xml) { + var parser = new ol.parser.ogc.WFS_v1_0_0(); + + var insertFeature = new ol.Feature({ + the_geom: new ol.geom.MultiPoint([[1, 2]]), + foo: 'bar', + nul: null + }); + var inserts = [insertFeature]; + var updateFeature = new ol.Feature({ + the_geom: new ol.geom.MultiPoint([[1, 2]]), + foo: 'bar', + // null value gets Property element with no Value + nul: null, + // undefined value means don't create a Property element + unwritten: undefined + }); + updateFeature.setId('fid.42'); + var updates = [updateFeature]; + + var deleteFeature = new ol.Feature(); + deleteFeature.setId('fid.37'); + var deletes = [deleteFeature]; + + var output = parser.writeTransaction(inserts, updates, deletes, { + featureNS: 'http://www.openplans.org/topp', + featureType: 'states', + featurePrefix: 'topp' + }); + expect(goog.dom.xml.loadXml(output)).to.xmleql(xml); done(); }); }); @@ -52,7 +82,7 @@ describe('ol.parser.ogc.WFS', function() { var url = 'spec/ol/parser/ogc/xml/wfs_v1/Native.xml'; afterLoadXml(url, function(xml) { var p = new ol.parser.ogc.WFS_v1_1_0(); - var output = p.write(null, {nativeElements: [{ + var output = p.writeTransaction(null, null, null, {nativeElements: [{ vendorId: 'ORACLE', safeToIgnore: true, value: 'ALTER SESSION ENABLE PARALLEL DML' @@ -70,14 +100,12 @@ describe('ol.parser.ogc.WFS', function() { var url = 'spec/ol/parser/ogc/xml/wfs_v1/GetFeatureMultiple.xml'; afterLoadXml(url, function(xml) { var p = new ol.parser.ogc.WFS_v1_0_0(); - var output = p.writers[p.defaultNamespaceURI]['GetFeature']. - apply(p, [{ - featureNS: 'http://www.openplans.org/topp', - featureTypes: ['states', 'cities'], - featurePrefix: 'topp' - } - ]); - expect(goog.dom.xml.loadXml(p.serialize(output))).to.xmleql(xml); + var output = p.writeGetFeature({ + featureNS: 'http://www.openplans.org/topp', + featureTypes: ['states', 'cities'], + featurePrefix: 'topp' + }); + expect(goog.dom.xml.loadXml(output)).to.xmleql(xml); done(); }); }); @@ -87,6 +115,8 @@ describe('ol.parser.ogc.WFS', function() { }); goog.require('goog.dom.xml'); +goog.require('ol.Feature'); +goog.require('ol.geom.MultiPoint'); goog.require('ol.parser.ogc.WFS'); goog.require('ol.parser.ogc.WFS_v1_0_0'); goog.require('ol.parser.ogc.WFS_v1_1_0'); diff --git a/test/spec/ol/parser/ogc/xml/wfs_v1/Transaction.xml b/test/spec/ol/parser/ogc/xml/wfs_v1/Transaction.xml index b147dc07f4..98527bfc2d 100644 --- a/test/spec/ol/parser/ogc/xml/wfs_v1/Transaction.xml +++ b/test/spec/ol/parser/ogc/xml/wfs_v1/Transaction.xml @@ -1 +1 @@ - + diff --git a/test/spec/ol/parser/ogc/xml/wfs_v1/TransactionMulti.xml b/test/spec/ol/parser/ogc/xml/wfs_v1/TransactionMulti.xml new file mode 100644 index 0000000000..22c0886045 --- /dev/null +++ b/test/spec/ol/parser/ogc/xml/wfs_v1/TransactionMulti.xml @@ -0,0 +1,45 @@ + + + + + + + + 1,2 + + + + + bar + + + + + the_geom + + + + + 1,2 + + + + + + + foo + bar + + + nul + + + + + + + + + + + \ No newline at end of file