1 /** 2 * Copyright 2016 Google Inc. All Rights Reserved. 3 * 4 * <p>Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 5 * except in compliance with the License. You may obtain a copy of the License at 6 * 7 * <p>http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * <p>Unless required by applicable law or agreed to in writing, software distributed under the 10 * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 * express or implied. See the License for the specific language governing permissions and 12 * limitations under the License. 13 */ 14 package com.android.vts.util; 15 16 import com.android.vts.entity.DeviceInfoEntity; 17 import com.android.vts.entity.TestRunEntity; 18 import com.google.appengine.api.datastore.Entity; 19 import com.google.appengine.api.datastore.Key; 20 import com.google.appengine.api.datastore.KeyFactory; 21 import com.google.appengine.api.datastore.Query.CompositeFilterOperator; 22 import com.google.appengine.api.datastore.Query.Filter; 23 import com.google.appengine.api.datastore.Query.FilterOperator; 24 import com.google.appengine.api.datastore.Query.FilterPredicate; 25 import java.util.ArrayList; 26 import java.util.EnumSet; 27 import java.util.HashMap; 28 import java.util.List; 29 import java.util.Map; 30 import java.util.concurrent.TimeUnit; 31 import java.util.regex.Matcher; 32 import java.util.regex.Pattern; 33 34 /** FilterUtil, a helper class for parsing and matching search queries to data. */ 35 public class FilterUtil { 36 private static final String TERM_DELIMITER = ":"; 37 private static final String SEARCH_REGEX = "([^\"]\\S*|\".+?\")\\s*"; 38 39 /** Key class to represent a filter token. */ 40 public static enum FilterKey { 41 DEVICE_BUILD_ID("devicebuildid", DeviceInfoEntity.BUILD_ID), 42 BRANCH("branch", DeviceInfoEntity.BRANCH), 43 TARGET("target", DeviceInfoEntity.BUILD_FLAVOR), 44 DEVICE("device", DeviceInfoEntity.PRODUCT), 45 VTS_BUILD_ID("vtsbuildid", TestRunEntity.TEST_BUILD_ID), 46 HOSTNAME("hostname", TestRunEntity.HOST_NAME), 47 PASSING("passing", TestRunEntity.PASS_COUNT), 48 NONPASSING("nonpassing", TestRunEntity.FAIL_COUNT); 49 50 private static final Map<String, FilterKey> keyMap; 51 52 static { 53 keyMap = new HashMap<>(); 54 for (FilterKey k : EnumSet.allOf(FilterKey.class)) { k.toString()55 keyMap.put(k.toString(), k); 56 } 57 } 58 59 /** 60 * Test if a string is a valid key. 61 * 62 * @param keyString The key string. 63 * @return True if they key string matches a key. 64 */ isKey(String keyString)65 public static boolean isKey(String keyString) { 66 return keyMap.containsKey(keyString); 67 } 68 69 /** 70 * Parses a key string into a key. 71 * 72 * @param keyString The key string. 73 * @return The key matching the key string. 74 */ parse(String keyString)75 public static FilterKey parse(String keyString) { 76 return keyMap.get(keyString); 77 } 78 79 private final String keyString; 80 private final String property; 81 82 /** 83 * Constructs a key with the specified key string. 84 * 85 * @param keyString The identifying key string. 86 * @param propertyName The name of the property to match. 87 */ FilterKey(String keyString, String propertyName)88 private FilterKey(String keyString, String propertyName) { 89 this.keyString = keyString; 90 this.property = propertyName; 91 } 92 93 @Override toString()94 public String toString() { 95 return this.keyString; 96 } 97 getFilterForString(String matchString)98 public Filter getFilterForString(String matchString) { 99 return new FilterPredicate(this.property, FilterOperator.EQUAL, matchString); 100 } 101 getFilterForNumber(long matchNumber)102 public Filter getFilterForNumber(long matchNumber) { 103 return new FilterPredicate(this.property, FilterOperator.EQUAL, matchNumber); 104 } 105 } 106 107 /** 108 * Gets a device filter from the user search string. 109 * 110 * @param searchString The user search string. 111 * @return A filter to apply to a test run's device entities. 112 */ getDeviceFilter(String searchString)113 public static Filter getDeviceFilter(String searchString) { 114 Filter deviceFilter = null; 115 if (searchString != null) { 116 Matcher m = Pattern.compile(SEARCH_REGEX).matcher(searchString); 117 while (m.find()) { 118 String term = m.group(1).replace("\"", ""); 119 if (!term.contains(TERM_DELIMITER)) 120 continue; 121 String[] terms = term.split(TERM_DELIMITER, 2); 122 if (terms.length != 2 || !FilterKey.isKey(terms[0].toLowerCase())) 123 continue; 124 125 FilterKey key = FilterKey.parse(terms[0].toLowerCase()); 126 switch (key) { 127 case BRANCH: 128 case DEVICE: 129 case DEVICE_BUILD_ID: 130 case TARGET: 131 String value = terms[1].toLowerCase(); 132 Filter f = key.getFilterForString(value); 133 if (deviceFilter == null) { 134 deviceFilter = f; 135 } else { 136 deviceFilter = CompositeFilterOperator.and(deviceFilter, f); 137 } 138 break; 139 default: 140 break; 141 } 142 } 143 } 144 return deviceFilter; 145 } 146 147 /** 148 * Get a filter on the test run type. 149 * 150 * @param showPresubmit True to display presubmit tests. 151 * @param showPostsubmit True to display postsubmit tests. 152 * @param unfiltered True if no filtering should be applied. 153 * @return A filter on the test type. 154 */ getTestTypeFilter( boolean showPresubmit, boolean showPostsubmit, boolean unfiltered)155 public static Filter getTestTypeFilter( 156 boolean showPresubmit, boolean showPostsubmit, boolean unfiltered) { 157 if (unfiltered) { 158 return null; 159 } else if (showPresubmit && !showPostsubmit) { 160 return new FilterPredicate(TestRunEntity.TYPE, FilterOperator.EQUAL, 161 TestRunEntity.TestRunType.PRESUBMIT.getNumber()); 162 } else if (showPostsubmit && !showPresubmit) { 163 return new FilterPredicate(TestRunEntity.TYPE, FilterOperator.EQUAL, 164 TestRunEntity.TestRunType.POSTSUBMIT.getNumber()); 165 } else { 166 List<Integer> types = new ArrayList<>(); 167 types.add(TestRunEntity.TestRunType.PRESUBMIT.getNumber()); 168 types.add(TestRunEntity.TestRunType.POSTSUBMIT.getNumber()); 169 return new FilterPredicate(TestRunEntity.TYPE, FilterOperator.IN, types); 170 } 171 } 172 173 /** 174 * Get a filter on test runs from a user search string. 175 * 176 * @param searchString The user search string to parse. 177 * @param runTypeFilter An existing filter on test runs, or null. 178 * @return A filter with the values from the user search string. 179 */ getUserTestFilter(String searchString, Filter runTypeFilter)180 public static Filter getUserTestFilter(String searchString, Filter runTypeFilter) { 181 Filter testRunFilter = runTypeFilter; 182 if (searchString != null) { 183 Matcher m = Pattern.compile(SEARCH_REGEX).matcher(searchString); 184 while (m.find()) { 185 String term = m.group(1).replace("\"", ""); 186 if (!term.contains(TERM_DELIMITER)) 187 continue; 188 String[] terms = term.split(TERM_DELIMITER, 2); 189 if (terms.length != 2 || !FilterKey.isKey(terms[0].toLowerCase())) 190 continue; 191 192 FilterKey key = FilterKey.parse(terms[0].toLowerCase()); 193 Filter f = null; 194 switch (key) { 195 case NONPASSING: 196 case PASSING: 197 String valueString = terms[1]; 198 try { 199 Long value = Long.parseLong(valueString); 200 f = key.getFilterForNumber(value); 201 } catch (NumberFormatException e) { 202 // invalid number 203 } 204 break; 205 case HOSTNAME: 206 case VTS_BUILD_ID: 207 String value = terms[1].toLowerCase(); 208 f = key.getFilterForString(value); 209 break; 210 default: 211 break; 212 } 213 if (testRunFilter == null) { 214 testRunFilter = f; 215 } else if (f != null) { 216 testRunFilter = CompositeFilterOperator.and(testRunFilter, f); 217 } 218 } 219 } 220 return testRunFilter; 221 } 222 223 /** 224 * Get the time range filter to apply to a query. 225 * 226 * @param testKey The key of the parent TestEntity object. 227 * @param startTime The start time in microseconds, or null if unbounded. 228 * @param endTime The end time in microseconds, or null if unbounded. 229 * @param testRunFilter The existing filter on test runs to apply, or null. 230 * @return A filter to apply on test runs. 231 */ getTimeFilter( Key testKey, Long startTime, Long endTime, Filter testRunFilter)232 public static Filter getTimeFilter( 233 Key testKey, Long startTime, Long endTime, Filter testRunFilter) { 234 if (startTime == null && endTime == null) { 235 endTime = TimeUnit.MILLISECONDS.toMicros(System.currentTimeMillis()); 236 } 237 238 Filter startFilter = null; 239 Filter endFilter = null; 240 Filter filter = null; 241 if (startTime != null) { 242 Key startKey = KeyFactory.createKey(testKey, TestRunEntity.KIND, startTime); 243 startFilter = new FilterPredicate( 244 Entity.KEY_RESERVED_PROPERTY, FilterOperator.GREATER_THAN_OR_EQUAL, startKey); 245 filter = startFilter; 246 } 247 if (endTime != null) { 248 Key endKey = KeyFactory.createKey(testKey, TestRunEntity.KIND, endTime); 249 endFilter = new FilterPredicate( 250 Entity.KEY_RESERVED_PROPERTY, FilterOperator.LESS_THAN_OR_EQUAL, endKey); 251 filter = endFilter; 252 } 253 if (startFilter != null && endFilter != null) { 254 filter = CompositeFilterOperator.and(startFilter, endFilter); 255 } 256 if (testRunFilter != null) { 257 filter = CompositeFilterOperator.and(filter, testRunFilter); 258 } 259 return filter; 260 } 261 getTimeFilter(Key testKey, Long startTime, Long endTime)262 public static Filter getTimeFilter(Key testKey, Long startTime, Long endTime) { 263 return getTimeFilter(testKey, startTime, endTime, null); 264 } 265 } 266