From 38500021b6a567418dc88cabec5fff7d50c8850e Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Fri, 27 Mar 2015 14:48:04 -0700 Subject: [PATCH] GUAC-1138: Add filter pattern tokenizer. Match any object property against all tokens. --- .../webapp/app/list/types/FilterPattern.js | 71 ++++++-- .../main/webapp/app/list/types/FilterToken.js | 153 ++++++++++++++++++ 2 files changed, 207 insertions(+), 17 deletions(-) create mode 100644 guacamole/src/main/webapp/app/list/types/FilterToken.js diff --git a/guacamole/src/main/webapp/app/list/types/FilterPattern.js b/guacamole/src/main/webapp/app/list/types/FilterPattern.js index 82fdfc27e..61192b84e 100644 --- a/guacamole/src/main/webapp/app/list/types/FilterPattern.js +++ b/guacamole/src/main/webapp/app/list/types/FilterPattern.js @@ -23,8 +23,14 @@ /** * A service for defining the FilterPattern class. */ -angular.module('list').factory('FilterPattern', ['$parse', - function defineFilterPattern($parse) { +angular.module('list').factory('FilterPattern', ['$injector', + function defineFilterPattern($injector) { + + // Required types + var FilterToken = $injector.get('FilterToken'); + + // Required services + var $parse = $injector.get('$parse'); /** * Object which handles compilation of filtering predicates as used by @@ -69,6 +75,43 @@ angular.module('list').factory('FilterPattern', ['$parse', getters.push($parse(expression)); }); + /** + * Determines whether the given object matches the given filter pattern + * token. + * + * @param {Object} object + * The object to match the token against. + * + * @param {FilterToken} token + * The token from the tokenized filter pattern to match aginst the + * given object. + * + * @returns {Boolean} + * true if the object matches the token, false otherwise. + */ + var matchesToken = function matchToken(object, token) { + + // Only match against literals + if (token.type !== 'LITERAL') + return false; + + // For each defined getter + for (var i=0; i < getters.length; i++) { + + // Retrieve value of current getter + var value = getters[i](object); + + // If the value matches the pattern, the whole object matches + if (String(value).toLowerCase().indexOf(token.value) !== -1) + return true; + + } + + // No matches found + return false; + + }; + /** * The current filtering predicate. * @@ -92,26 +135,20 @@ angular.module('list').factory('FilterPattern', ['$parse', return; } - // Convert to lower case for case insensitive matching - pattern = pattern.toLowerCase(); + // Tokenize pattern, converting to lower case for case-insensitive matching + var tokens = FilterToken.tokenize(pattern.toLowerCase()); // Return predicate which matches against the value of any getter in the getters array - filterPattern.predicate = function matchAny(object) { - - // For each defined getter - for (var i=0; i < getters.length; i++) { - - // Retrieve value of current getter - var value = getters[i](object); - - // If the value matches the pattern, the whole object matches - if (String(value).toLowerCase().indexOf(pattern) !== -1) - return true; + filterPattern.predicate = function matchesAllTokens(object) { + // False if any token does not match + for (var i=0; i < tokens.length; i++) { + if (!matchesToken(object, tokens[i])) + return false; } - // No matches found - return false; + // True if all tokens matched + return true; }; diff --git a/guacamole/src/main/webapp/app/list/types/FilterToken.js b/guacamole/src/main/webapp/app/list/types/FilterToken.js new file mode 100644 index 000000000..ae8aa1881 --- /dev/null +++ b/guacamole/src/main/webapp/app/list/types/FilterToken.js @@ -0,0 +1,153 @@ +/* + * Copyright (C) 2015 Glyptodon LLC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +/** + * A service for defining the FilterToken class. + */ +angular.module('list').factory('FilterToken', [ + function defineFilterToken() { + + /** + * An arbitrary token having an associated type and string value. + * + * @constructor + * @param {String} type + * The type of this token. Each legal type name is a property within + * FilterToken.Types. + * + * @param {String} value + * The string value of this token. + */ + var FilterToken = function FilterToken(type, value) { + + /** + * The type of this token. Each legal type name is a property within + * FilterToken.Types. + * + * @type String + */ + this.type = type; + + /** + * The string value of this token. + * + * @type String + */ + this.value = value; + + }; + + /** + * All legal token types, and corresponding regular expressions which match + * them. If the regular expression contains capturing groups, the last + * matching group will be used as the value of the token. + * + * @type Object. + */ + FilterToken.Types = { + + /** + * A string literal. + */ + LITERAL: /^"([^"]*)"|^\S+/, + + /** + * Arbitrary contiguous whitespace. + */ + WHITESPACE: /^\s+/ + + }; + + /** + * Tokenizes the given string, returning an array of tokens. Whitespace + * tokens are dropped. + * + * @param {String} str + * The string to tokenize. + * + * @returns {FilterToken[]} + * All tokens identified within the given string, in order. + */ + FilterToken.tokenize = function tokenize(str) { + + var tokens = []; + + /** + * Returns the first token on the current string, removing the token + * from that string. + * + * @returns FilterToken + * The first token on the string, or null if no tokens match. + */ + var popToken = function popToken() { + + // Attempt to find a matching token + for (var type in FilterToken.Types) { + + // Get regular expression for current type + var regex = FilterToken.Types[type]; + + // If token matches, return the matching group + var match = regex.exec(str); + if (match) { + + // Advance to next token + str = str.substring(match[0].length); + + // Grab last matching group + var matchingGroup = match[0]; + for (var i=1; i < match.length; i++) + matchingGroup = match[i] || matchingGroup; + + // Return new token + return new FilterToken(type, matchingGroup); + + } + + } + + // No match + return null; + + }; + + // Tokenize input until no input remains + while (str) { + + // Remove first token + var token = popToken(); + if (!token) + break; + + // Add token to tokens array, if not whitespace + if (token.type !== 'WHITESPACE') + tokens.push(token); + + } + + return tokens; + + }; + + return FilterToken; + +}]); \ No newline at end of file