Home | History | Annotate | Download | only in net_internals
      1 // Copyright (c) 2013 The Chromium Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style license that can be
      3 // found in the LICENSE file.
      4 
      5 var SourceFilterParser = (function() {
      6   'use strict';
      7 
      8   /**
      9    * Parses |filterText|, extracting a sort method, a list of filters, and a
     10    * copy of |filterText| with all sort parameters removed.
     11    */
     12   function SourceFilterParser(filterText) {
     13     // Final output will be stored here.
     14     this.filter = null;
     15     this.sort = {};
     16     this.filterTextWithoutSort = '';
     17     var filterList = parseFilter_(filterText);
     18 
     19     // Text filters are stored here as strings and then added as a function at
     20     // the end, for performance reasons.
     21     var textFilters = [];
     22 
     23     // Filter functions are first created individually, and then merged.
     24     var filterFunctions = [];
     25 
     26     for (var i = 0; i < filterList.length; ++i) {
     27       var filterElement = filterList[i].parsed;
     28       var negated = filterList[i].negated;
     29 
     30       var sort = parseSortDirective_(filterElement, negated);
     31       if (sort) {
     32         this.sort = sort;
     33         continue;
     34       }
     35 
     36       this.filterTextWithoutSort += filterList[i].original;
     37 
     38       var filter = parseRestrictDirective_(filterElement, negated);
     39       if (!filter)
     40         filter = parseStringDirective_(filterElement, negated);
     41       if (filter) {
     42         if (negated) {
     43           filter = (function(func, sourceEntry) {
     44             return !func(sourceEntry);
     45           }).bind(null, filter);
     46         }
     47         filterFunctions.push(filter);
     48         continue;
     49       }
     50       textFilters.push({ text: filterElement, negated: negated });
     51     }
     52 
     53     // Create a single filter for all text filters, so they can share a
     54     // TabePrinter.
     55     filterFunctions.push(textFilter_.bind(null, textFilters));
     56 
     57     // Create function to go through all the filters.
     58     this.filter = function(sourceEntry) {
     59       for (var i = 0; i < filterFunctions.length; ++i) {
     60         if (!filterFunctions[i](sourceEntry))
     61           return false;
     62       }
     63       return true;
     64     };
     65   }
     66 
     67   /**
     68    * Parses a single "sort:" directive, and returns a dictionary containing
     69    * the sort function and direction.  Returns null on failure, including
     70    * the case when no such sort function exists.
     71    */
     72   function parseSortDirective_(filterElement, backwards) {
     73     var match = /^sort:(.*)$/.exec(filterElement);
     74     if (!match)
     75       return null;
     76     return { method: match[1], backwards: backwards };
     77   }
     78 
     79   /**
     80    * Tries to parses |filterElement| as a single "is:" directive, and returns a
     81    * new filter function.  Returns null on failure.
     82    */
     83   function parseRestrictDirective_(filterElement) {
     84     var match = /^is:(.*)$/.exec(filterElement);
     85     if (!match)
     86       return null;
     87     if (match[1] == 'active') {
     88       return function(sourceEntry) { return !sourceEntry.isInactive(); };
     89     }
     90     if (match[1] == 'error') {
     91       return function(sourceEntry) { return sourceEntry.isError(); };
     92     }
     93     return null;
     94   }
     95 
     96   /**
     97    * Tries to parse |filterElement| as a single filter of a type that takes
     98    * arbitrary strings as input, and returns a new filter function on success.
     99    * Returns null on failure.
    100    */
    101   function parseStringDirective_(filterElement) {
    102     var match = RegExp('^([^:]*):(.*)$').exec(filterElement);
    103     if (!match)
    104       return null;
    105 
    106     // Split parameters around commas and remove empty elements.
    107     var parameters = match[2].split(',');
    108     parameters = parameters.filter(function(string) {
    109       return string.length > 0;
    110     });
    111 
    112     if (match[1] == 'type') {
    113       return function(sourceEntry) {
    114         var i;
    115         var sourceType = sourceEntry.getSourceTypeString().toLowerCase();
    116         for (i = 0; i < parameters.length; ++i) {
    117           if (sourceType.search(parameters[i]) != -1)
    118             return true;
    119         }
    120         return false;
    121       };
    122     }
    123 
    124     if (match[1] == 'id') {
    125       return function(sourceEntry) {
    126         return parameters.indexOf(sourceEntry.getSourceId() + '') != -1;
    127       };
    128     }
    129 
    130     return null;
    131   }
    132 
    133   /**
    134    * Takes in the text of a filter and returns a list of
    135    * {parsed, original, negated} values that correspond to substrings of the
    136    * filter before and after filtering, and whether or not it started with a
    137    * '-'.  Extra whitespace other than a single character after each element is
    138    * ignored.  Parsed strings are all lowercase.
    139    */
    140   function parseFilter_(filterText) {
    141     // Assemble a list of quoted and unquoted strings in the filter.
    142     var filterList = [];
    143     var position = 0;
    144     while (position < filterText.length) {
    145       var inQuote = false;
    146       var filterElement = '';
    147       var negated = false;
    148       var startPosition = position;
    149       while (position < filterText.length) {
    150         var nextCharacter = filterText[position];
    151         ++position;
    152         if (nextCharacter == '\\' &&
    153             position < filterText.length) {
    154           // If there's a backslash, skip the backslash and add the next
    155           // character to the element.
    156           filterElement += filterText[position];
    157           ++position;
    158           continue;
    159         } else if (nextCharacter == '"') {
    160           // If there's an unescaped quote character, toggle |inQuote| without
    161           // modifying the element.
    162           inQuote = !inQuote;
    163         } else if (!inQuote && /\s/.test(nextCharacter)) {
    164           // If not in a quote and have a whitespace character, that's the
    165           // end of the element.
    166           break;
    167         } else if (nextCharacter == '-' && startPosition == position - 1) {
    168           // If this is the first character, and it's a '-', this entry is
    169           // negated.
    170           negated = true;
    171         } else {
    172           // Otherwise, add the next character to the element.
    173           filterElement += nextCharacter;
    174         }
    175       }
    176 
    177       if (filterElement.length > 0) {
    178         var filter = {
    179           parsed: filterElement.toLowerCase(),
    180           original: filterText.substring(startPosition, position),
    181           negated: negated,
    182         };
    183         filterList.push(filter);
    184       }
    185     }
    186     return filterList;
    187   }
    188 
    189   /**
    190    * Takes in a list of text filters and a SourceEntry.  Each filter has
    191    * "text" and "negated" fields.  Returns true if the SourceEntry matches all
    192    * filters in the (possibly empty) list.
    193    */
    194   function textFilter_(textFilters, sourceEntry) {
    195     var tablePrinter = null;
    196     for (var i = 0; i < textFilters.length; ++i) {
    197       var text = textFilters[i].text;
    198       var negated = textFilters[i].negated;
    199       var match = false;
    200       // The description is often not contained in one of the log entries.
    201       // The source type almost never is, so check for them directly.
    202       var description = sourceEntry.getDescription().toLowerCase();
    203       var type = sourceEntry.getSourceTypeString().toLowerCase();
    204       if (description.indexOf(text) != -1 || type.indexOf(text) != -1) {
    205         match = true;
    206       } else {
    207         if (!tablePrinter)
    208           tablePrinter = sourceEntry.createTablePrinter();
    209         match = tablePrinter.search(text);
    210       }
    211       if (negated)
    212         match = !match;
    213       if (!match)
    214         return false;
    215     }
    216     return true;
    217   }
    218 
    219   return SourceFilterParser;
    220 })();
    221