(function() { var Ext = window.Ext4 || window.Ext; /** * @private * Parses an ALM compatible query string */ Ext.define('Rally.data.util.QueryStringParser', { config: { /** * @cfg {String} (required) * The string to parse into a {Rally.data.wsapi.Filter} */ string: null }, constructor: function (config) { this.initConfig(config); }, /** * @private * {RegEx} The operation regex */ operations: /^\s*(=|!=|<=|>=|<|>|AND|OR|[!]?CONTAINS|\(|\))/i, /** * @private * {RegEx} The quote regex */ quotes: /^"([^"\\]*(\\.[^"\\]*)*)"/, /** * @private * {RegEx} The word regex */ words: /^\s*([^ \)]+)/, /** * @private * {Array[String]} Array of valid operations to parse from a string expression */ operators: ['=','!=','<=','>=','<','>','AND','OR','!CONTAINS','CONTAINS'], /** * @private * {RegEx} The regex used to determine if the property's value should be converted to a String */ convertToStringPropertyMatcher: /.*Name.*/, /** * Find the next token * @private * @return {String} */ peek: function () { var string = Ext.String.trim(this.string), matches; matches = this.operations.exec(string); if (matches && matches.length) { return matches[1]; } matches = this.quotes.exec(string); if (matches && matches.length) { return matches[1]; } matches = this.words.exec(string); if (matches && matches.length) { return matches[1]; } return ''; }, /** * Remove the stringToFind from the string to move onto the next token * @private * @param {String} stringToFind * @return {String} */ consume: function (stringToFind) { var string = Ext.String.trim(this.string); this.string = string.substring(string.indexOf(stringToFind) + stringToFind.length); }, /** * Parse the next term in the string * @private * @return {String|Rally.data.wsapi.Filter} */ parseNextTerm: function () { var nextTerm = this.peek(); if (nextTerm === '(') { this.consume('('); var expression = this.applyOperators(this.operators); if (!(expression instanceof Rally.data.wsapi.Filter)) { throw new Error('Invalid expression starting at "' + expression + '"'); } this.consume(')'); return expression; } else { this.consume(nextTerm); return nextTerm; } }, applyOperators: function (operators, operator) { if (!operators.length) { return this.parseNextTerm(); } if (!operator) { operator = operators[0]; } var property = this.applyOperators(operators.slice(1)); while (this.peek() === operator) { this.consume(operator); var value = this.applyOperators(operators.slice(1)); property = Ext.create('Rally.data.wsapi.Filter', { property: property, operator: operator, value: this._convertToType(value, property) }); } return property; }, /** * Make sure we are aware of types * @param value * @private */ _convertToType: function (value, property) { if (!Ext.isString(value)) { return value; } else if (this.convertToStringPropertyMatcher.test(property)) { return value.toString(); } else if (value.toLowerCase() === 'true') { return true; } else if (value.toLowerCase() === 'false') { return false; } else if (value.length > 0 && !isNaN(+value)) { return +value; } else if (value === 'null') { return null; } else { return value; } }, /** * Convert the operations in the provided string to uppercase * @private * @return {String} */ _operatorsToUpperCase: function (string) { string = string.replace(/\)\s+and\s+\(/ig, ') AND ('); string = string.replace(/\)\s+or\s+\(/ig, ') OR ('); return string.replace(/\b(contains)/ig, 'CONTAINS'); }, /** * Parse the expression provided if possible * @return {Rally.data.wsapi.Filter} */ parseExpression: function () { var originalString = this.string; if (this.string === null) { throw new Error ('Cannot parse null query: ' + originalString); } this.string = Ext.String.trim(this.string); this.string = this._operatorsToUpperCase(this.string); var filter = this.parseNextTerm(); if (!(filter instanceof Rally.data.wsapi.Filter)) { throw new Error ('Failed to parse query: ' + originalString); } var trailingQuery = this.parseNextTerm(); if(trailingQuery) { throw new Error ('Failed to parse query: found trailing info at "' + trailingQuery + '". Make sure you wrap the whole query in parentheses.'); } return filter; } }); })();