diff options
Diffstat (limited to 'lib/caldav')
-rw-r--r-- | lib/caldav/query_builder.js | 318 |
1 files changed, 318 insertions, 0 deletions
diff --git a/lib/caldav/query_builder.js b/lib/caldav/query_builder.js new file mode 100644 index 0000000..3d7c9eb --- /dev/null +++ b/lib/caldav/query_builder.js @@ -0,0 +1,318 @@ +/** +@namespace +*/ +(function(module, ns) { + var Template = ns.require('template'); + + /** + * Builds a node of a calendar-data or filter xml element. + * + * @param {QueryBuilder} builder instance. + * @param {String} name component/prop name (like RRULE/VEVENT). + * @param {Boolean} isProp is this node a property tag? + */ + function Node(builder, name, isProp) { + this.name = name; + this.builder = builder; + this.isProp = !!isProp; + + this.comps = Object.create(null); + this.props = Object.create(null); + } + + Node.prototype = { + + /** + * Hook for adding custom node content. + * (for unsupported or custom filters) + * + * Usually you never want to use this. + * + * @type {Null|Array} + */ + content: null, + + /** + * Appends custom string content into node. + * + * @param {String} string content. + */ + appendString: function(string) { + if (!this.content) { + this.content = []; + } + + if (typeof(string) !== 'string') { + string = string.toString(); + } + + content.push(string); + }, + + _timeRange: null, + + /** + * Adds a time range element to the node. + * + * Example: + * + * var node; + * + * // key/values not validated or converted + * // but directly piped into the time-range element. + * node.setTimeRange({ + * start: '20060104T000000Z', + * end: '20060105T000000Z' + * }); + * + * // when null removes element + * node.setTimeRange(null); + * + * @param {Object|Null} range time range or null to remove. + */ + setTimeRange: function(range) { + this._timeRange = range; + }, + + /** + * Removes a property from the output. + * @param {String} name prop. + */ + removeProp: function(name) { + delete this.props[name]; + }, + + /** + * Removes a component from the output. + * + * @param {String} name comp. + */ + removeComp: function(name) { + delete this.comps[name]; + }, + + _addNodes: function(type, nodes) { + // return value when is array + var result = this; + + if (!Array.isArray(nodes)) { + // clear out the return value as we will + // now use the first node. + result = null; + } + + nodes = (Array.isArray(nodes)) ? nodes : [nodes]; + + var idx = 0; + var len = nodes.length; + var name; + var node; + + for (; idx < len; idx++) { + name = nodes[idx]; + node = new Node(this.builder, name, type === 'props'); + this[type][name] = node; + } + + // when we where not given an array of nodes + // assume we want one specific one so set that + // as the return value. + if (!result) + result = node; + + return result; + }, + + /** + * Adds one or more props. + * If property already exists will not add + * duplicates but return the existing property. + * + * @param {String|Array[String]} prop one or more properties to add. + * @return {Node|Self} returns a node or self when given an array. + */ + prop: function(prop) { + return this._addNodes('props', prop); + }, + + /** + * Adds one or more comp. + * If comp already exists will not add + * duplicates but return the existing comp. + * + * @param {String|Array[String]} comp one or more components to add. + * @return {Node|Self} returns a node or self when given an array. + */ + comp: function(comp) { + return this._addNodes('comps', comp); + }, + + xmlAttributes: function() { + return { name: this.name }; + }, + + /** + * Transform tree into a string. + * + * NOTE: order is not preserved at all here. + * It is highly unlikely that order is a + * factor for calendar-data or filter + * but this is fair warning for other uses. + */ + toString: function() { + var content = ''; + var key; + var template = this.builder.template; + + if (this._timeRange) { + content += template.tag( + ['caldav', 'time-range'], + this._timeRange + ); + } + + // render out children + for (key in this.props) { + content += this.props[key].toString(); + } + + for (key in this.comps) { + content += this.comps[key].toString(); + } + + if (this.content) { + content += this.content.join(''); + } + + // determine the tag name + var tag; + if (this.isProp) { + tag = this.builder.propTag; + } else { + tag = this.builder.compTag; + } + + // build the xml element and return it. + return template.tag( + tag, + this.xmlAttributes(), + content + ); + } + }; + + /** + * Query builder can be used to build xml document fragments + * for calendar-data & calendar-filter. + * (and any other xml document with a similar structure) + * + * Options: + * - template: (Caldav.Template instance) + * - tag: container tag (like 'calendar-data') + * - attributes: attributes for root + * - compTag: name of comp[onent] tag name (like 'comp') + * - propTag: name of property tag (like 'prop') + * + * @param {Object} options query builder options. + */ + function QueryBuilder(options) { + if (!options) + options = {}; + + if (!(options.template instanceof Template)) { + throw new TypeError( + '.template must be an instance' + + ' of Caldav.Template given "' + options.template + '"' + ); + } + + for (var key in options) { + if (options.hasOwnProperty(key)) { + this[key] = options[key]; + } + } + } + + QueryBuilder.prototype = { + tag: ['caldav', 'calendar-data'], + + compTag: ['caldav', 'comp'], + + propTag: ['caldav', 'prop'], + + attributes: null, + + _limitRecurrenceSet: null, + + /** + * Adds the recurrence set limit child to the query. + * Directly maps to the caldav 'limit-recurrence-set' element. + * + * Examples: + * + * var builder; + * + * // no validation or formatting is done. + * builder.setRecurrenceSetLimit({ + * start: '20060103T000000Z', + * end: '20060103T000000Z' + * }); + * + * // use null to clear value + * builder.setRecurrenceSetLimit(null); + * + * @param {Object|Null} limit see above. + */ + setRecurrenceSetLimit: function(limit) { + this._limitRecurrenceSet = limit; + }, + + /** + * @param {String} name component name (like VCALENDAR). + * @return {QueryBuilder.Node} node instance. + */ + setComp: function(name) { + return this._compRoot = new Node(this, name); + }, + + /** + * Returns the root node of the document fragment. + */ + getComp: function() { + return this._compRoot; + }, + + toString: function() { + var content = ''; + var comp = this.getComp(); + + if (this._limitRecurrenceSet) { + content += this.template.tag( + ['caldav', 'limit-recurrence-set'], + this._limitRecurrenceSet + ); + } + + if (comp) { + content += comp.toString(); + } + + return this.template.tag( + this.tag, + this.attributes, + content + ); + } + + }; + + QueryBuilder.Node = Node; + module.exports = QueryBuilder; + +}.apply( + this, + (this.Caldav) ? + [Caldav('query_builder'), Caldav] : + [module, require('./caldav')] +)); + |