1 /* 2 * Licensed to the Apache Software Foundation (ASF) under one or more 3 * contributor license agreements. See the NOTICE file distributed with 4 * this work for additional information regarding copyright ownership. 5 * The ASF licenses this file to You under the Apache License, Version 2.0 6 * (the "License"); you may not use this file except in compliance with 7 * the License. You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 * 17 */ 18 19 package org.apache.jmeter.extractor; 20 21 import java.io.Serializable; 22 import java.util.ArrayList; 23 import java.util.Iterator; 24 import java.util.LinkedList; 25 import java.util.List; 26 27 import org.apache.commons.lang.StringEscapeUtils; 28 import org.apache.jmeter.processor.PostProcessor; 29 import org.apache.jmeter.samplers.SampleResult; 30 import org.apache.jmeter.testelement.AbstractTestElement; 31 import org.apache.jmeter.testelement.property.IntegerProperty; 32 import org.apache.jmeter.threads.JMeterContext; 33 import org.apache.jmeter.threads.JMeterVariables; 34 import org.apache.jmeter.util.JMeterUtils; 35 import org.apache.jorphan.logging.LoggingManager; 36 import org.apache.log.Logger; 37 import org.apache.oro.text.MalformedCachePatternException; 38 import org.apache.oro.text.regex.MatchResult; 39 import org.apache.oro.text.regex.Pattern; 40 import org.apache.oro.text.regex.PatternMatcher; 41 import org.apache.oro.text.regex.PatternMatcherInput; 42 import org.apache.oro.text.regex.Perl5Compiler; 43 import org.apache.oro.text.regex.Perl5Matcher; 44 import org.apache.oro.text.regex.Util; 45 46 // @see org.apache.jmeter.extractor.TestRegexExtractor for unit tests 47 48 public class RegexExtractor extends AbstractTestElement implements PostProcessor, Serializable { 49 50 51 private static final Logger log = LoggingManager.getLoggerForClass(); 52 53 // What to match against. N.B. do not change the string value or test plans will break! 54 private static final String MATCH_AGAINST = "RegexExtractor.useHeaders"; // $NON-NLS-1$ 55 /* 56 * Permissible values: 57 * true - match against headers 58 * false or absent - match against body (this was the original default) 59 * URL - match against URL 60 * These are passed to the setUseField() method 61 * 62 * Do not change these values! 63 */ 64 public static final String USE_HDRS = "true"; // $NON-NLS-1$ 65 public static final String USE_BODY = "false"; // $NON-NLS-1$ 66 public static final String USE_BODY_UNESCAPED = "unescaped"; // $NON-NLS-1$ 67 public static final String USE_URL = "URL"; // $NON-NLS-1$ 68 public static final String USE_CODE = "code"; // $NON-NLS-1$ 69 public static final String USE_MESSAGE = "message"; // $NON-NLS-1$ 70 71 72 private static final String REGEX = "RegexExtractor.regex"; // $NON-NLS-1$ 73 74 private static final String REFNAME = "RegexExtractor.refname"; // $NON-NLS-1$ 75 76 private static final String MATCH_NUMBER = "RegexExtractor.match_number"; // $NON-NLS-1$ 77 78 private static final String DEFAULT = "RegexExtractor.default"; // $NON-NLS-1$ 79 80 private static final String TEMPLATE = "RegexExtractor.template"; // $NON-NLS-1$ 81 82 private static final String REF_MATCH_NR = "_matchNr"; // $NON-NLS-1$ 83 84 private static final String UNDERSCORE = "_"; // $NON-NLS-1$ 85 86 private Object[] template = null; 87 88 /** 89 * Parses the response data using regular expressions and saving the results 90 * into variables for use later in the test. 91 * 92 * @see org.apache.jmeter.processor.PostProcessor#process() 93 */ 94 public void process() { 95 initTemplate(); 96 JMeterContext context = getThreadContext(); 97 SampleResult previousResult = context.getPreviousResult(); 98 if (previousResult == null) { 99 return; 100 } 101 log.debug("RegexExtractor processing result"); 102 103 // Fetch some variables 104 JMeterVariables vars = context.getVariables(); 105 String refName = getRefName(); 106 int matchNumber = getMatchNumber(); 107 108 final String defaultValue = getDefaultValue(); 109 if (defaultValue.length() > 0){// Only replace default if it is provided 110 vars.put(refName, defaultValue); 111 } 112 113 Perl5Matcher matcher = JMeterUtils.getMatcher(); 114 String inputString = 115 useUrl() ? previousResult.getUrlAsString() // Bug 39707 116 : useHeaders() ? previousResult.getResponseHeaders() 117 : useCode() ? previousResult.getResponseCode() //Bug 43451 118 : useMessage() ? previousResult.getResponseMessage() //Bug 43451 119 : useUnescapedBody() ? StringEscapeUtils.unescapeHtml(previousResult.getResponseDataAsString()) 120 : previousResult.getResponseDataAsString() // Bug 36898 121 ; 122 if (log.isDebugEnabled()) { 123 log.debug("Input = " + inputString); 124 } 125 PatternMatcherInput input = new PatternMatcherInput(inputString); 126 String regex = getRegex(); 127 if (log.isDebugEnabled()) { 128 log.debug("Regex = " + regex); 129 } 130 try { 131 Pattern pattern = JMeterUtils.getPatternCache().getPattern(regex, Perl5Compiler.READ_ONLY_MASK); 132 List matches = new ArrayList(); 133 int x = 0; 134 boolean done = false; 135 do { 136 if (matcher.contains(input, pattern)) { 137 log.debug("RegexExtractor: Match found!"); 138 matches.add(matcher.getMatch()); 139 } else { 140 done = true; 141 } 142 x++; 143 } while (x != matchNumber && !done); 144 145 int prevCount = 0; 146 String prevString = vars.get(refName + REF_MATCH_NR); 147 if (prevString != null) { 148 vars.remove(refName + REF_MATCH_NR);// ensure old value is not left defined 149 try { 150 prevCount = Integer.parseInt(prevString); 151 } catch (NumberFormatException e1) { 152 log.warn("Could not parse "+prevString+" "+e1); 153 } 154 } 155 int matchCount=0;// Number of refName_n variable sets to keep 156 try { 157 MatchResult match; 158 if (matchNumber >= 0) {// Original match behaviour 159 match = getCorrectMatch(matches, matchNumber); 160 if (match != null) { 161 vars.put(refName, generateResult(match)); 162 saveGroups(vars, refName, match); 163 } else { 164 // refname has already been set to the default (if present) 165 removeGroups(vars, refName); 166 } 167 } else // < 0 means we save all the matches 168 { 169 removeGroups(vars, refName); // remove any single matches 170 matchCount = matches.size(); 171 vars.put(refName + REF_MATCH_NR, Integer.toString(matchCount));// Save the count 172 for (int i = 1; i <= matchCount; i++) { 173 match = getCorrectMatch(matches, i); 174 if (match != null) { 175 final String refName_n = new StringBuffer(refName).append(UNDERSCORE).append(i).toString(); 176 vars.put(refName_n, generateResult(match)); 177 saveGroups(vars, refName_n, match); 178 } 179 } 180 } 181 // Remove any left-over variables 182 for (int i = matchCount + 1; i <= prevCount; i++) { 183 final String refName_n = new StringBuffer(refName).append(UNDERSCORE).append(i).toString(); 184 vars.remove(refName_n); 185 removeGroups(vars, refName_n); 186 } 187 } catch (RuntimeException e) { 188 log.warn("Error while generating result"); 189 } 190 } catch (MalformedCachePatternException e) { 191 log.warn("Error in pattern: " + regex); 192 } 193 } 194 195 /** 196 * Creates the variables:<br/> 197 * basename_gn, where n=0...# of groups<br/> 198 * basename_g = number of groups (apart from g0) 199 */ 200 private void saveGroups(JMeterVariables vars, String basename, MatchResult match) { 201 StringBuffer buf = new StringBuffer(); 202 buf.append(basename); 203 buf.append("_g"); // $NON-NLS-1$ 204 int pfxlen=buf.length(); 205 String prevString=vars.get(buf.toString()); 206 int previous=0; 207 if (prevString!=null){ 208 try { 209 previous=Integer.parseInt(prevString); 210 } catch (NumberFormatException e) { 211 log.warn("Could not parse "+prevString+" "+e); 212 } 213 } 214 //Note: match.groups() includes group 0 215 final int groups = match.groups(); 216 for (int x = 0; x < groups; x++) { 217 buf.append(x); 218 vars.put(buf.toString(), match.group(x)); 219 buf.setLength(pfxlen); 220 } 221 vars.put(buf.toString(), Integer.toString(groups-1)); 222 for (int i = groups; i <= previous; i++){ 223 buf.append(i); 224 vars.remove(buf.toString());// remove the remaining _gn vars 225 buf.setLength(pfxlen); 226 } 227 } 228 229 /** 230 * Removes the variables:<br/> 231 * basename_gn, where n=0...# of groups<br/> 232 * basename_g = number of groups (apart from g0) 233 */ 234 private void removeGroups(JMeterVariables vars, String basename) { 235 StringBuffer buf = new StringBuffer(); 236 buf.append(basename); 237 buf.append("_g"); // $NON-NLS-1$ 238 int pfxlen=buf.length(); 239 // How many groups are there? 240 int groups; 241 try { 242 groups=Integer.parseInt(vars.get(buf.toString())); 243 } catch (NumberFormatException e) { 244 groups=0; 245 } 246 vars.remove(buf.toString());// Remove the group count 247 for (int i = 0; i <= groups; i++) { 248 buf.append(i); 249 vars.remove(buf.toString());// remove the g0,g1...gn vars 250 buf.setLength(pfxlen); 251 } 252 } 253 254 public Object clone() { 255 RegexExtractor cloned = (RegexExtractor) super.clone(); 256 cloned.template = this.template; 257 return cloned; 258 } 259 260 private String generateResult(MatchResult match) { 261 StringBuffer result = new StringBuffer(); 262 for (int a = 0; a < template.length; a++) { 263 log.debug("RegexExtractor: Template piece #" + a + " = " + template[a]); 264 if (template[a] instanceof String) { 265 result.append(template[a]); 266 } else { 267 result.append(match.group(((Integer) template[a]).intValue())); 268 } 269 } 270 log.debug("Regex Extractor result = " + result.toString()); 271 return result.toString(); 272 } 273 274 private void initTemplate() { 275 if (template != null) { 276 return; 277 } 278 List pieces = new ArrayList(); 279 List combined = new LinkedList(); 280 String rawTemplate = getTemplate(); 281 PatternMatcher matcher = JMeterUtils.getMatcher(); 282 Pattern templatePattern = JMeterUtils.getPatternCache().getPattern("\\$(\\d+)\\$" // $NON-NLS-1$ 283 , Perl5Compiler.READ_ONLY_MASK 284 & Perl5Compiler.SINGLELINE_MASK); 285 log.debug("Pattern = " + templatePattern); 286 log.debug("template = " + rawTemplate); 287 Util.split(pieces, matcher, templatePattern, rawTemplate); 288 PatternMatcherInput input = new PatternMatcherInput(rawTemplate); 289 boolean startsWith = isFirstElementGroup(rawTemplate); 290 log.debug("template split into " + pieces.size() + " pieces, starts with = " + startsWith); 291 if (startsWith) { 292 pieces.remove(0);// Remove initial empty entry 293 } 294 Iterator iter = pieces.iterator(); 295 while (iter.hasNext()) { 296 boolean matchExists = matcher.contains(input, templatePattern); 297 if (startsWith) { 298 if (matchExists) { 299 combined.add(new Integer(matcher.getMatch().group(1))); 300 } 301 combined.add(iter.next()); 302 } else { 303 combined.add(iter.next()); 304 if (matchExists) { 305 combined.add(new Integer(matcher.getMatch().group(1))); 306 } 307 } 308 } 309 if (matcher.contains(input, templatePattern)) { 310 log.debug("Template does end with template pattern"); 311 combined.add(new Integer(matcher.getMatch().group(1))); 312 } 313 template = combined.toArray(); 314 } 315 316 private boolean isFirstElementGroup(String rawData) { 317 try { 318 Pattern pattern = JMeterUtils.getPatternCache().getPattern("^\\$\\d+\\$" // $NON-NLS-1$ 319 , Perl5Compiler.READ_ONLY_MASK 320 & Perl5Compiler.SINGLELINE_MASK); 321 return (JMeterUtils.getMatcher()).contains(rawData, pattern); 322 } catch (RuntimeException e) { 323 log.error("", e); 324 return false; 325 } 326 } 327 328 /** 329 * Grab the appropriate result from the list. 330 * 331 * @param matches 332 * list of matches 333 * @param entry 334 * the entry number in the list 335 * @return MatchResult 336 */ 337 private MatchResult getCorrectMatch(List matches, int entry) { 338 int matchSize = matches.size(); 339 340 if (matchSize <= 0 || entry > matchSize){ 341 return null; 342 } 343 344 if (entry == 0) // Random match 345 { 346 return (MatchResult) matches.get(JMeterUtils.getRandomInt(matchSize)); 347 } 348 349 return (MatchResult) matches.get(entry - 1); 350 } 351 352 public void setRegex(String regex) { 353 setProperty(REGEX, regex); 354 } 355 356 public String getRegex() { 357 return getPropertyAsString(REGEX); 358 } 359 360 public void setRefName(String refName) { 361 setProperty(REFNAME, refName); 362 } 363 364 public String getRefName() { 365 return getPropertyAsString(REFNAME); 366 } 367 368 /** 369 * Set which Match to use. This can be any positive number, indicating the 370 * exact match to use, or 0, which is interpreted as meaning random. 371 * 372 * @param matchNumber 373 */ 374 public void setMatchNumber(int matchNumber) { 375 setProperty(new IntegerProperty(MATCH_NUMBER, matchNumber)); 376 } 377 378 public void setMatchNumber(String matchNumber) { 379 setProperty(MATCH_NUMBER, matchNumber); 380 } 381 382 public int getMatchNumber() { 383 return getPropertyAsInt(MATCH_NUMBER); 384 } 385 386 public String getMatchNumberAsString() { 387 return getPropertyAsString(MATCH_NUMBER); 388 } 389 390 /** 391 * Sets the value of the variable if no matches are found 392 * 393 * @param defaultValue 394 */ 395 public void setDefaultValue(String defaultValue) { 396 setProperty(DEFAULT, defaultValue); 397 } 398 399 public String getDefaultValue() { 400 return getPropertyAsString(DEFAULT); 401 } 402 403 public void setTemplate(String template) { 404 setProperty(TEMPLATE, template); 405 } 406 407 public String getTemplate() { 408 return getPropertyAsString(TEMPLATE); 409 } 410 411 public boolean useHeaders() { 412 return USE_HDRS.equalsIgnoreCase( getPropertyAsString(MATCH_AGAINST)); 413 } 414 415 // Allow for property not yet being set (probably only applies to Test cases) 416 public boolean useBody() { 417 String prop = getPropertyAsString(MATCH_AGAINST); 418 return prop.length()==0 || USE_BODY.equalsIgnoreCase(prop);// $NON-NLS-1$ 419 } 420 421 public boolean useUnescapedBody() { 422 String prop = getPropertyAsString(MATCH_AGAINST); 423 return USE_BODY_UNESCAPED.equalsIgnoreCase(prop);// $NON-NLS-1$ 424 } 425 426 public boolean useUrl() { 427 String prop = getPropertyAsString(MATCH_AGAINST); 428 return USE_URL.equalsIgnoreCase(prop); 429 } 430 431 public boolean useCode() { 432 String prop = getPropertyAsString(MATCH_AGAINST); 433 return USE_CODE.equalsIgnoreCase(prop); 434 } 435 436 public boolean useMessage() { 437 String prop = getPropertyAsString(MATCH_AGAINST); 438 return USE_MESSAGE.equalsIgnoreCase(prop); 439 } 440 441 public void setUseField(String actionCommand) { 442 setProperty(MATCH_AGAINST,actionCommand); 443 } 444 }