From 112aa9c8d0b5929dbd0d27535c6605bc13ab1c27 Mon Sep 17 00:00:00 2001 From: Diego Ferrand Date: Mon, 15 Apr 2024 11:09:30 -0300 Subject: [PATCH] Release 2.4 (#63) --- pom.xml | 8 +- .../core/analysis/AnalysisReporter.java | 4 +- .../core/automatic/CorrelationHistory.java | 65 ++- .../core/automatic/ExtractionSuggestion.java | 11 +- .../core/automatic/FileManagementUtils.java | 6 + .../core/automatic/JMeterElementUtils.java | 84 +++- .../core/automatic/ReplacementSuggestion.java | 8 +- .../core/extractors/CorrelationExtractor.java | 30 ++ .../extractors/JsonCorrelationExtractor.java | 195 +++++++-- .../extractors/RegexCorrelationExtractor.java | 32 +- .../replacements/CorrelationReplacement.java | 43 ++ .../JsonCorrelationReplacement.java | 323 ++++++++++++++ .../RegexCorrelationReplacement.java | 51 +-- .../suggestions/method/ComparisonMethod.java | 2 +- .../gui/CorrelationComponentsRegistry.java | 10 +- .../gui/CorrelationRulePartPanel.java | 12 +- .../CorrelationTemplatesSelectionPanel.java | 12 +- .../automatic/CorrelationHistoryFrame.java | 31 +- .../CorrelationSuggestionsPanel.java | 17 +- .../gui/automatic/CorrelationWizard.java | 4 - .../FunctionCorrelationReplacement.html | 2 +- .../JsonCorrelationExtractor.html | 20 +- .../JsonCorrelationReplacement.html | 46 ++ .../MoreExtractor.html | 2 +- .../MoreReplacement.html | 2 +- .../NoneExtractor.html | 2 +- .../NoneReplacement.html | 2 +- .../RegexCorrelationExtractor.html | 2 +- .../RegexCorrelationReplacement.html | 2 +- .../SiebelCounterCorrelationReplacement.html | 2 +- .../SiebelRowCorrelationExtractor.html | 2 +- .../SiebelRowIdCorrelationReplacement.html | 2 +- ...SiebelRowParamsCorrelationReplacement.html | 2 +- .../XmlCorrelationExtractor.html | 2 +- .../automatic/CorrelationHistoryTest.java | 50 +-- .../method/ComparableJMeterVariables.java | 3 +- .../JsonCorrelationExtractorTest.java | 410 ++++++++++++++++++ .../RegexCorrelationExtractorTest.java | 36 +- .../JsonCorrelationReplacementTest.java | 399 +++++++++++++++++ .../method/ComparisonMethodTest.java | 3 +- .../CorrelationComponentsRegistryTest.java | 12 +- .../automatic/CorrelationHistoryFrameIT.java | 22 + .../gui/automatic/CorrelationWizardIT.java | 3 +- .../RegexCorrelationExtractorDescription.html | 2 +- .../RegexCorrelationExtractorFullDisplay.html | 2 +- .../selectedReplacementDescription.html | 2 +- 46 files changed, 1736 insertions(+), 246 deletions(-) create mode 100644 src/main/java/com/blazemeter/jmeter/correlation/core/replacements/JsonCorrelationReplacement.java create mode 100644 src/main/resources/correlation-descriptions/JsonCorrelationReplacement.html create mode 100644 src/test/java/com/blazemeter/jmeter/correlation/core/extractors/JsonCorrelationExtractorTest.java create mode 100644 src/test/java/com/blazemeter/jmeter/correlation/core/replacements/JsonCorrelationReplacementTest.java diff --git a/pom.xml b/pom.xml index 6b91381..6d097c6 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.blazemeter jmeter-bzm-correlation-recorder jar - 2.3 + 2.4 Correlation Recorder as JMeter plugin Correlation Recorder Plugin for JMeter https://github.com/Blazemeter/CorrelationRecorder @@ -92,6 +92,12 @@ 2.13.0 provided + + com.jayway.jsonpath + json-path + 2.9.0 + provided + org.assertj assertj-core diff --git a/src/main/java/com/blazemeter/jmeter/correlation/core/analysis/AnalysisReporter.java b/src/main/java/com/blazemeter/jmeter/correlation/core/analysis/AnalysisReporter.java index 6b7e083..8cc87af 100644 --- a/src/main/java/com/blazemeter/jmeter/correlation/core/analysis/AnalysisReporter.java +++ b/src/main/java/com/blazemeter/jmeter/correlation/core/analysis/AnalysisReporter.java @@ -151,7 +151,7 @@ public static List generateCorrelationSuggestions() { } if (part instanceof CorrelationExtractor) { - RegexCorrelationExtractor extractor = (RegexCorrelationExtractor) part; + CorrelationExtractor extractor = (CorrelationExtractor) part; for (ReportEntry entry : report.entries) { ExtractionSuggestion extraction = new ExtractionSuggestion(extractor, entry.getSampler()); extraction.setValue(entry.value); @@ -161,7 +161,7 @@ public static List generateCorrelationSuggestions() { suggestion.addExtractionSuggestion(extraction); } } else { - RegexCorrelationReplacement replacement = (RegexCorrelationReplacement) part; + CorrelationReplacement replacement = (CorrelationReplacement) part; for (ReportEntry entry : report.entries) { ReplacementSuggestion replacementSuggestion = new ReplacementSuggestion(replacement, diff --git a/src/main/java/com/blazemeter/jmeter/correlation/core/automatic/CorrelationHistory.java b/src/main/java/com/blazemeter/jmeter/correlation/core/automatic/CorrelationHistory.java index 396f7b2..7b2ed70 100644 --- a/src/main/java/com/blazemeter/jmeter/correlation/core/automatic/CorrelationHistory.java +++ b/src/main/java/com/blazemeter/jmeter/correlation/core/automatic/CorrelationHistory.java @@ -32,6 +32,8 @@ public class CorrelationHistory { public static final String AUXILIARY_STEP_MESSAGE = "Auxiliary Iterarion (No Recording iteration found)"; public static final String ORIGINAL_RECORDING_MESSAGE = "Original Recording"; + public static final String CUSTOM_ITERATION_MESSAGE = "Checkpoint Iteration"; + public static final String FAILED_REPLAY_MESSAGE = "Replay result: %s requests pending resolution."; public static final String SUCCESS_REPLAY_TEMPLATE = "Replay %s"; @@ -69,8 +71,21 @@ public void addOriginalRecordingStep(String originalRecordingFilepath, steps = new ArrayList<>(); Step step = new Step(ORIGINAL_RECORDING_MESSAGE); - step.setTestPlanFilepath(originalRecordingFilepath); - step.setRecordingTraceFilepath(originalRecordingTraceFilepath); + step.setTestPlanFilepath( + FileManagementUtils.getRelativeFatherPath(originalRecordingFilepath)); + step.setRecordingTraceFilepath( + FileManagementUtils.getRelativeFatherPath(originalRecordingTraceFilepath)); + step.setNotes("Automatically generated iteration after recording"); + addStep(step); + } + + public void addCustomIteration() { + Step step = new Step(CUSTOM_ITERATION_MESSAGE); + step.setTestPlanFilepath( + FileManagementUtils.getRelativeFatherPath(saveCurrentTestPlanSupplier.get())); + step.setRecordingTraceFilepath( + FileManagementUtils.getRelativeFatherPath(recordingFilePathSupplier.get())); + step.setNotes("Checkpoint generated by user. [Double click to edit]"); addStep(step); } @@ -80,8 +95,11 @@ public void addSuccessfulReplay(String testPlanFilepath, String replayTraceFilep (hadErrors ? SUCCESS_REPLAY_POSTFIX_WITH_ERRORS : SUCCESS_REPLAY_POSTFIX_WITHOUT_ERRORS))); - step.setTestPlanFilepath(testPlanFilepath); - step.setReplayTraceFilepath(replayTraceFilepath); + step.setTestPlanFilepath( + FileManagementUtils.getRelativeFatherPath(testPlanFilepath)); + step.setReplayTraceFilepath( + FileManagementUtils.getRelativeFatherPath(replayTraceFilepath)); + step.setNotes("Automatically generated iteration after replay"); addStep(step); } @@ -97,15 +115,23 @@ public List getSteps() { public void addFailedReplay(String testPlanFilepath, String replayTraceFilepath, Integer newErrors) { Step step = new Step(String.format(FAILED_REPLAY_MESSAGE, newErrors)); - step.setTestPlanFilepath(testPlanFilepath); - step.setReplayTraceFilepath(replayTraceFilepath); + + step.setTestPlanFilepath( + FileManagementUtils.getRelativeFatherPath(testPlanFilepath)); + step.setReplayTraceFilepath( + FileManagementUtils.getRelativeFatherPath(replayTraceFilepath)); + step.setNotes("Automatically generated iteration after replay"); + addStep(step); } public void addAnalysisStep(String message, String testPlanFilepath, String traceFilepath) { Step step = new Step(message); - step.setTestPlanFilepath(testPlanFilepath); - step.setReplayTraceFilepath(traceFilepath); + step.setTestPlanFilepath( + FileManagementUtils.getRelativeFatherPath(testPlanFilepath)); + step.setReplayTraceFilepath( + FileManagementUtils.getRelativeFatherPath(traceFilepath)); + step.setNotes("Automatically generated iteration after correlation suggestions application"); addStep(step); } @@ -131,8 +157,10 @@ public Step getRecordingStep() { if (steps.isEmpty()) { LOG.error("CorrelationHistory has no iterations, forcing an auxiliary iteartion"); Step auxiliaryStep = new Step(AUXILIARY_STEP_MESSAGE); - auxiliaryStep.setTestPlanFilepath(saveCurrentTestPlanSupplier.get()); - auxiliaryStep.setRecordingTraceFilepath(recordingFilePathSupplier.get()); + auxiliaryStep.setTestPlanFilepath( + FileManagementUtils.getRelativeFatherPath(saveCurrentTestPlanSupplier.get())); + auxiliaryStep.setRecordingTraceFilepath( + FileManagementUtils.getRelativeFatherPath(recordingFilePathSupplier.get())); addStep(auxiliaryStep); } return steps.get(0); @@ -194,11 +222,11 @@ public void deleteSteps(List steps) { } public void addRestoredStep(Step step) { - Step newStep = new Step( - "Restored iteration with timestamp: %s", step.getTimestamp()); + Step newStep = new Step("Restored iteration"); newStep.setTestPlanFilepath(step.getTestPlanFilepath()); newStep.setRecordingTraceFilepath(step.getRecordingTraceFilepath()); newStep.setReplayTraceFilepath(step.getReplayTraceFilepath()); + newStep.setNotes("Reverted to the iteration with timestamp: " + step.getTimestamp()); this.steps.add(newStep); saveToFile(); } @@ -278,6 +306,7 @@ public static class Step { private String replayTraceFilepath; private String timestamp; private String fatherTimeStamp = null; + private String notes; @JsonIgnore private Supplier saveCurrentTestPlanSupplier = saveCurrentTestPlan(); @@ -309,6 +338,14 @@ public String getStepMessage() { return String.format(stepMessage, msgEnd); } + public String getNotes() { + return notes; + } + + public void setNotes(String notes) { + this.notes = notes; + } + @VisibleForTesting public void setStepMessage(String stepMessage) { this.stepMessage = stepMessage; @@ -343,7 +380,8 @@ public void setReplayTraceFilepath(String replayTraceFilepath) { } public void addCurrentTestPlan() { - setTestPlanFilepath(saveCurrentTestPlanSupplier.get()); + setTestPlanFilepath( + FileManagementUtils.getRelativeFatherPath(saveCurrentTestPlanSupplier.get())); } public String getTimestamp() { @@ -367,6 +405,7 @@ public String toString() { ", replayTraceFilepath='" + replayTraceFilepath + '\'' + ", time='" + timestamp + '\'' + ", fatherTimestamp='" + fatherTimeStamp + '\'' + + ", notes='" + notes + '\'' + '}'; } } diff --git a/src/main/java/com/blazemeter/jmeter/correlation/core/automatic/ExtractionSuggestion.java b/src/main/java/com/blazemeter/jmeter/correlation/core/automatic/ExtractionSuggestion.java index 69a758c..09e1036 100644 --- a/src/main/java/com/blazemeter/jmeter/correlation/core/automatic/ExtractionSuggestion.java +++ b/src/main/java/com/blazemeter/jmeter/correlation/core/automatic/ExtractionSuggestion.java @@ -11,7 +11,7 @@ public class ExtractionSuggestion { private List> extractors = new ArrayList<>(); //TODO:Remove the single extractor and only use the list - private RegexCorrelationExtractor extractor; + private CorrelationExtractor extractor; private SampleResult sampleResult; private String value; private String name; @@ -25,18 +25,13 @@ public ExtractionSuggestion(CorrelationExtractor extractors, SampleResult sam this.sampleResult = sampleResult; } - public ExtractionSuggestion(RegexCorrelationExtractor extractor, SampleResult sampleResult) { - this.extractor = extractor; - this.sampleResult = sampleResult; - } - - public ExtractionSuggestion(RegexCorrelationExtractor extractor, HTTPSamplerBase sampler) { + public ExtractionSuggestion(CorrelationExtractor extractor, HTTPSamplerBase sampler) { this.extractor = extractor; this.sampler = sampler; this.comesFromSampleResult = false; } - public RegexCorrelationExtractor getExtractor() { + public CorrelationExtractor getExtractor() { return extractor; } diff --git a/src/main/java/com/blazemeter/jmeter/correlation/core/automatic/FileManagementUtils.java b/src/main/java/com/blazemeter/jmeter/correlation/core/automatic/FileManagementUtils.java index e364982..a9ef8c5 100644 --- a/src/main/java/com/blazemeter/jmeter/correlation/core/automatic/FileManagementUtils.java +++ b/src/main/java/com/blazemeter/jmeter/correlation/core/automatic/FileManagementUtils.java @@ -58,6 +58,12 @@ private static void makeFolderAtBin(String name) { private static Path getPathInBin(String name) { return Paths.get(JMeterUtils.getJMeterBinDir(), name); } + + public static String getRelativeFatherPath(String fullPathStr) { + Path fullPath = Paths.get(fullPathStr); + return fullPath.getParent().toString() + File.separator + + fullPath.getFileName().toString(); + } public static void makeReplayResultsFolder() { makeFolderAtBin(REPLAY_FOLDER); diff --git a/src/main/java/com/blazemeter/jmeter/correlation/core/automatic/JMeterElementUtils.java b/src/main/java/com/blazemeter/jmeter/correlation/core/automatic/JMeterElementUtils.java index e12b0b8..51f4bcd 100644 --- a/src/main/java/com/blazemeter/jmeter/correlation/core/automatic/JMeterElementUtils.java +++ b/src/main/java/com/blazemeter/jmeter/correlation/core/automatic/JMeterElementUtils.java @@ -2,6 +2,8 @@ import com.blazemeter.jmeter.correlation.CorrelationProxyControl; import com.helger.commons.annotation.VisibleForTesting; +import com.jayway.jsonpath.InvalidPathException; +import com.jayway.jsonpath.JsonPath; import java.awt.BorderLayout; import java.awt.Dimension; import java.awt.Toolkit; @@ -155,7 +157,7 @@ public static String saveTestPlanConverted(HashTree testPlan, String name) { try { convertSubTree(testPlan); SaveService.saveTree(testPlan, - Files.newOutputStream(Paths.get(name))); + Files.newOutputStream(Paths.get(name))); LOG.info("Test Plan's Snapshot saved to {}", name); return name; } catch (IOException e) { @@ -166,7 +168,7 @@ public static String saveTestPlanConverted(HashTree testPlan, String name) { public static String saveTestPlanSnapshot() { return saveTestPlanConverted(getTreeModel().getTestPlan(), - FileManagementUtils.getSnapshotFileName()); + FileManagementUtils.getSnapshotFileName()); } public static String saveTestPlan(HashTree testPlan, String filename) { @@ -241,7 +243,7 @@ private boolean isIgnoredHeader(String key) { return configuration.getIgnoredHeaders().stream().anyMatch(key::equalsIgnoreCase); } - static boolean isJson(String value) { + public static boolean isJson(String value) { try { new JSONObject(value); } catch (JSONException ex) { @@ -466,6 +468,73 @@ public void extractParametersFromJsonArray(JSONArray array, Map> jsonFindMatches(String input, String jsonpath) { + Object result = null; + ArrayList matches = new ArrayList<>(); + try { + result = JsonPath.read(input, jsonpath); + } catch (InvalidPathException e) { + // When no match, no report error, only no return any data + } + if (result == null) { + return Pair.of(null, matches); + } else if (result instanceof net.minidev.json.JSONArray) { + net.minidev.json.JSONArray results = (net.minidev.json.JSONArray) result; + for (Object value : results) { + matches.add(toJsonString(value)); + } + } else if (canBeString(result)) { + matches.add(toJsonString(result)); + } else if (result instanceof Map) { + LOG.warn( + "Valued returned by JSONPath is a json object and not a text value, " + + "return value is null"); + return Pair.of(result.getClass(), matches); + } else { + LOG.warn( + "Valued returned by JSONPath is not supported, return value is null"); + } + return Pair.of(result != null ? result.getClass() : null, matches); + } + /** * Obtains the HTTPArgument from a JMeterProperty element. * Note: this method requires that the JMeterProperty is an instance of HTTPArgument. @@ -639,7 +708,7 @@ public static void refreshJMeter() { * * @param path The file path of the JMeter test plan to load. * @return A HashTree representing the structure of the loaded JMeter test plan. - * If an error occurs during loading, this method will return null. + * If an error occurs during loading, this method will return null. */ public static HashTree getTestPlan(String path) { HashTree hashTree = new HashTree(); @@ -1105,9 +1174,9 @@ private static List getCorrelationProxyControllers(JMeterTreeMod } /* - * Reminder: This method is not properly working (or at least the generated nodes - * are not properly working) - * */ + * Reminder: This method is not properly working (or at least the generated nodes + * are not properly working) + * */ public static List getSamplerNodes(HashTree testPlan) { /* Reminder: We commented this code because we have the theory that @@ -1139,4 +1208,5 @@ public static JMeterTreeModel getCurrentJMeterTreeModel() { } return model; } + } diff --git a/src/main/java/com/blazemeter/jmeter/correlation/core/automatic/ReplacementSuggestion.java b/src/main/java/com/blazemeter/jmeter/correlation/core/automatic/ReplacementSuggestion.java index b97cda6..2bfa368 100644 --- a/src/main/java/com/blazemeter/jmeter/correlation/core/automatic/ReplacementSuggestion.java +++ b/src/main/java/com/blazemeter/jmeter/correlation/core/automatic/ReplacementSuggestion.java @@ -1,22 +1,22 @@ package com.blazemeter.jmeter.correlation.core.automatic; -import com.blazemeter.jmeter.correlation.core.replacements.RegexCorrelationReplacement; +import com.blazemeter.jmeter.correlation.core.replacements.CorrelationReplacement; import org.apache.jmeter.testelement.TestElement; public class ReplacementSuggestion { - private final RegexCorrelationReplacement replacementSuggestion; + private final CorrelationReplacement replacementSuggestion; private final TestElement usage; private String source; private String value; private String name; - public ReplacementSuggestion(RegexCorrelationReplacement replacementSuggestion, + public ReplacementSuggestion(CorrelationReplacement replacementSuggestion, TestElement usage) { this.replacementSuggestion = replacementSuggestion; this.usage = usage; } - public RegexCorrelationReplacement getReplacementSuggestion() { + public CorrelationReplacement getReplacementSuggestion() { return replacementSuggestion; } diff --git a/src/main/java/com/blazemeter/jmeter/correlation/core/extractors/CorrelationExtractor.java b/src/main/java/com/blazemeter/jmeter/correlation/core/extractors/CorrelationExtractor.java index 62c1b04..00a0372 100644 --- a/src/main/java/com/blazemeter/jmeter/correlation/core/extractors/CorrelationExtractor.java +++ b/src/main/java/com/blazemeter/jmeter/correlation/core/extractors/CorrelationExtractor.java @@ -3,6 +3,7 @@ import com.blazemeter.jmeter.correlation.core.CorrelationContext; import com.blazemeter.jmeter.correlation.core.CorrelationRulePartTestElement; import com.blazemeter.jmeter.correlation.core.DescriptionContent; +import com.blazemeter.jmeter.correlation.core.analysis.AnalysisReporter; import com.blazemeter.jmeter.correlation.core.templates.CorrelationRuleSerializationPropertyFilter; import com.blazemeter.jmeter.correlation.gui.CorrelationRuleTestElement; import com.fasterxml.jackson.annotation.JsonFilter; @@ -10,7 +11,12 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.JsonTypeInfo.Id; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import java.util.regex.Pattern; import org.apache.jmeter.protocol.http.sampler.HTTPSamplerBase; import org.apache.jmeter.samplers.SampleResult; import org.apache.jmeter.testelement.AbstractTestElement; @@ -42,6 +48,10 @@ public abstract class CorrelationExtractor extends protected static final String TARGET_FIELD_NAME = EXTRACTOR_PREFIX + "target"; protected static final String TARGET_FIELD_DESCRIPTION = "Target"; protected static final String REGEX_DEFAULT_VALUE = "param=\"(.+?)\""; + protected static final String DEFAULT_EXTRACTOR_SUFFIX = "_NOT_FOUND"; + + private static final Function VARIABLE_PATTERN_PROVIDER = + (variableName) -> Pattern.compile(variableName + "_(\\d|matchNr)"); private static final Logger LOG = LoggerFactory .getLogger(CorrelationRuleSerializationPropertyFilter.class); public transient String variableName; @@ -156,4 +166,24 @@ public String toString() { public List createPostProcessors(String variableName, int i) { return new ArrayList<>(); } + + protected void clearJMeterVariables(JMeterVariables vars) { + Set> entries = new HashSet<>(vars.entrySet()); + entries.forEach(e -> { + if (VARIABLE_PATTERN_PROVIDER.apply(variableName).matcher(e.getKey()).matches()) { + vars.remove(e.getKey()); + } + }); + } + + /** + * Used to provide information about the extraction of values from the response. + * This method would be used, when we are performing an analysis, regardless + * of the mode of the recording (i.e. whether we are recording or doing + * static analysis). + */ + protected void analyze(String value, Object affectedElement, String varName) { + AnalysisReporter.report(this, value, affectedElement, varName, target.name()); + } + } diff --git a/src/main/java/com/blazemeter/jmeter/correlation/core/extractors/JsonCorrelationExtractor.java b/src/main/java/com/blazemeter/jmeter/correlation/core/extractors/JsonCorrelationExtractor.java index d7fc050..15e2ba9 100644 --- a/src/main/java/com/blazemeter/jmeter/correlation/core/extractors/JsonCorrelationExtractor.java +++ b/src/main/java/com/blazemeter/jmeter/correlation/core/extractors/JsonCorrelationExtractor.java @@ -1,52 +1,92 @@ package com.blazemeter.jmeter.correlation.core.extractors; +import static com.blazemeter.jmeter.correlation.core.automatic.JMeterElementUtils.jsonFindMatches; + import com.blazemeter.jmeter.correlation.core.BaseCorrelationContext; import com.blazemeter.jmeter.correlation.core.CorrelationContext; import com.blazemeter.jmeter.correlation.core.ParameterDefinition; +import com.blazemeter.jmeter.correlation.core.analysis.AnalysisReporter; import com.blazemeter.jmeter.correlation.gui.CorrelationRuleTestElement; import com.helger.commons.annotation.VisibleForTesting; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; import java.util.List; import java.util.Objects; -import org.apache.jmeter.extractor.json.jmespath.JMESPathExtractor; -import org.apache.jmeter.extractor.json.jmespath.gui.JMESPathExtractorGui; +import org.apache.commons.lang3.tuple.Pair; import org.apache.jmeter.extractor.json.jsonpath.JSONPostProcessor; import org.apache.jmeter.protocol.http.sampler.HTTPSamplerBase; import org.apache.jmeter.samplers.SampleResult; import org.apache.jmeter.testelement.AbstractTestElement; import org.apache.jmeter.testelement.TestElement; import org.apache.jmeter.threads.JMeterVariables; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class JsonCorrelationExtractor extends CorrelationExtractor { private static final String DEFAULT_MATCH_NUMBER_NAME = "match number"; private static final int DEFAULT_MATCH_NUMBER = 1; - private static final String EXTRACTOR_PATH_NAME = EXTRACTOR_PREFIX + "path"; + private static final String EXTRACTOR_PATH_NAME = EXTRACTOR_PREFIX + "jsonpath"; private static final String MATCH_NUMBER_NAME = EXTRACTOR_PREFIX + "matchNr"; - private static final String EXTRACTOR_DESCRIPTION = "JSON extractor"; + private static final String MULTIVALUED_NAME = EXTRACTOR_PREFIX + "multiValued"; + private static final String EXTRACTOR_DESCRIPTION = "JSONPath expression"; private static final String MATCH_NUMBER_DESCRIPTION = "Match number"; - private static final String PATH_DEFAULT_VALUE = "$.param"; + private static final String MULTIVALUED_DESCRIPTION = "Multivalued"; + private static final boolean DEFAULT_MULTIVALUED = false; + private static final String PATH_DEFAULT_VALUE = "$.jsonpath.expression"; private static final ResultField DEFAULT_TARGET_VALUE = ResultField.BODY; - private String path; - private int matchNr; + + private static final Logger LOG = LoggerFactory.getLogger(JsonCorrelationExtractor.class); + private static final String JSON_EXTRACTOR_GUI_CLASS = + org.apache.jmeter.extractor.json.jsonpath.gui.JSONPostProcessorGui.class.getName(); + protected String jsonpath; + protected int matchNr; + protected boolean multiValued; + + private transient JMeterVariables currentVars; + private transient List currentSamplersChild; public JsonCorrelationExtractor() { matchNr = DEFAULT_MATCH_NUMBER; - path = PATH_DEFAULT_VALUE; + jsonpath = PATH_DEFAULT_VALUE; target = ResultField.BODY; + multiValued = DEFAULT_MULTIVALUED; } @VisibleForTesting public JsonCorrelationExtractor(String path, String variableName) { - this.path = path; + this.jsonpath = path; this.variableName = variableName; target = ResultField.BODY; matchNr = DEFAULT_MATCH_NUMBER; } + @VisibleForTesting + public JsonCorrelationExtractor(String path, int matchNr, ResultField target) { + this.jsonpath = path; + this.target = target; + this.matchNr = matchNr; + } + + @VisibleForTesting + public JsonCorrelationExtractor(String path) { + this.jsonpath = path; + this.matchNr = DEFAULT_MATCH_NUMBER; + this.target = ResultField.BODY; + this.multiValued = DEFAULT_MULTIVALUED; + } + + @VisibleForTesting + public JsonCorrelationExtractor(String path, int matchNr, String target, String multiValued) { + this.jsonpath = path; + this.matchNr = matchNr; + this.target = ResultField.valueOf(target); + this.multiValued = Boolean.parseBoolean(multiValued); + } + public void setPath(String path) { - this.path = path; + this.jsonpath = path; } @Override @@ -56,43 +96,51 @@ public String getDisplayName() { @Override public List getParams() { - return Arrays.asList(path, Integer.toString(matchNr), target.name()); + return Arrays.asList(jsonpath, Integer.toString(matchNr), target.name(), + Boolean.toString(multiValued)); } @Override public void setParams(List params) { - path = params.size() > 0 ? params.get(0) : "$.param"; + jsonpath = params.size() > 0 ? params.get(0) : "$.jsonpath.expression"; matchNr = params.size() > 1 ? parseInteger(params.get(1), DEFAULT_MATCH_NUMBER_NAME, DEFAULT_MATCH_NUMBER) : DEFAULT_MATCH_NUMBER; - target = params.size() > 3 && !params.get(3).isEmpty() ? ResultField.valueOf(params.get(3)) + target = params.size() > 2 && !params.get(2).isEmpty() ? ResultField.valueOf(params.get(2)) : DEFAULT_TARGET_VALUE; + multiValued = params.size() >= 3 && Boolean.parseBoolean(params.get(3)); } @Override public List getParamsDefinition() { + HashMap options = new HashMap(); + options.put(ResultField.BODY.name(), ResultField.BODY.getCode()); return Arrays.asList(new ParameterDefinition.TextParameterDefinition(EXTRACTOR_PATH_NAME, EXTRACTOR_DESCRIPTION, - REGEX_DEFAULT_VALUE), + PATH_DEFAULT_VALUE), new ParameterDefinition.TextParameterDefinition(MATCH_NUMBER_NAME, MATCH_NUMBER_DESCRIPTION, String.valueOf(DEFAULT_MATCH_NUMBER), true), new ParameterDefinition.ComboParameterDefinition(TARGET_FIELD_NAME, TARGET_FIELD_DESCRIPTION, - ResultField.BODY.name(), ResultField.getNamesToCodesMapping(), true)); + ResultField.BODY.name(), options, true), + new ParameterDefinition.CheckBoxParameterDefinition(MULTIVALUED_NAME, + MULTIVALUED_DESCRIPTION, + DEFAULT_MULTIVALUED, true)); } @Override public void updateTestElem(CorrelationRuleTestElement testElem) { super.updateTestElem(testElem); - testElem.setProperty(EXTRACTOR_PATH_NAME, path); + testElem.setProperty(EXTRACTOR_PATH_NAME, jsonpath); testElem.setProperty(MATCH_NUMBER_NAME, String.valueOf(matchNr)); testElem.setProperty(TARGET_FIELD_NAME, target != null ? target.name() : ResultField.BODY.name()); + testElem.setProperty(MULTIVALUED_NAME, multiValued); } @Override public void update(CorrelationRuleTestElement testElem) { super.update(testElem); - path = testElem.getPropertyAsString(EXTRACTOR_PATH_NAME); + jsonpath = testElem.getPropertyAsString(EXTRACTOR_PATH_NAME); matchNr = getMatchNumber(testElem); } @@ -105,9 +153,85 @@ public void process(HTTPSamplerBase sampler, List children, SampleR // If we can successfully parse the JSON and apply the path, we add a PostProcessor // to extract the value + if (jsonpath.isEmpty()) { + return; + } + if (matchNr == 0) { + LOG.warn("Extracting random appearances is not supported. Returning null instead."); + return; + } + String field = target.getField(result); + if (field == null || field.isEmpty()) { + return; + } + + this.currentVars = vars; + this.currentSamplersChild = children; + + String varName = multiValued ? generateVariableName() : variableName; + String matchedValue = null; + String matchedVariable = varName; + String matchedVariableChildPP = varName; + String matchedVariablePP = varName; + int matchedMatchNr = matchNr; + if (matchNr >= 0) { + String match = findMatch(field, matchNr); + if (match != null && !match.equals(vars.get(varName))) { + matchedValue = match; + } + } else { + Pair> resultMatches = jsonFindMatches(field, jsonpath); + ArrayList matches = resultMatches.getRight(); + if (matches.size() == 1) { + matchedValue = matches.get(0); + matchedMatchNr = 1; + } else if (matches.size() > 1) { + if (!multiValued) { + clearJMeterVariables(vars); + } + matchedValue = String.valueOf(matches.size()); + matchedVariableChildPP = matchedVariable + "_matchNr"; + int matchNr = 1; + for (String match : matches) { + vars.put(varName + "_" + matchNr, match); + matchNr++; + } + } + } + if (matchedValue != null) { + analyze(matchedValue, sampler, variableName); + addVarAndChildPostProcessor(matchedValue, matchedVariableChildPP, + createPostProcessor(matchedVariablePP, matchedMatchNr)); + } + } + + public String findMatch(String input, int matchNumber) { + if (!input.isEmpty()) { + Pair> resultMatches = jsonFindMatches(input, jsonpath); + ArrayList matches = resultMatches.getRight(); + if (matches.size() == 0) { + return null; + } + if (matches.size() >= matchNumber) { + return matches.get(matchNumber - 1); + } + LOG.warn("Match number {} is bigger than actual matches {}", + matchNumber, matches.size()); + } + return null; + } + + public AbstractTestElement createPostProcessor(String varName, int matchNr) { JSONPostProcessor jsonPostProcessor = new JSONPostProcessor(); - jsonPostProcessor.setJsonPathExpressions(path); + jsonPostProcessor.setProperty(TestElement.GUI_CLASS, JSON_EXTRACTOR_GUI_CLASS); + jsonPostProcessor.setName("JSON Path - " + varName); + jsonPostProcessor.setRefNames(varName); + jsonPostProcessor.setJsonPathExpressions(jsonpath); jsonPostProcessor.setComputeConcatenation(true); + jsonPostProcessor.setMatchNumbers(String.valueOf(matchNr)); + jsonPostProcessor.setDefaultValues(varName + DEFAULT_EXTRACTOR_SUFFIX); + jsonPostProcessor.setScopeAll(); + return jsonPostProcessor; } public void setRefName(String name) { @@ -115,7 +239,8 @@ public void setRefName(String name) { } private static int getMatchNumber(CorrelationRuleTestElement testElem) { - return parseInteger(testElem.getPropertyAsString(MATCH_NUMBER_NAME), DEFAULT_MATCH_NUMBER_NAME, + return parseInteger(testElem.getPropertyAsString(MATCH_NUMBER_NAME), + DEFAULT_MATCH_NUMBER_NAME, 1); } @@ -125,7 +250,7 @@ public Class getSupportedContext() { } public String getPath() { - return path; + return jsonpath; } public int getMatchNr() { @@ -139,23 +264,33 @@ public void setMatchNr(int matchNr) { @Override public String toString() { return "JsonCorrelationExtractor{" - + "path='" + path + '\'' + + "path='" + jsonpath + '\'' + ", matchNr=" + matchNr + ", variableName='" + variableName + '\'' + ", target=" + target + '}'; } + private void addVarAndChildPostProcessor(String match, String variableName, + AbstractTestElement postProcessor) { + if (AnalysisReporter.canCorrelate()) { + currentSamplersChild.add(postProcessor); + } + currentVars.put(variableName, match); + } + + private String generateVariableName() { + return variableName + "#" + context.getNextVariableNr(variableName); + } + + public void setMultiValued(boolean multiValued) { + this.multiValued = multiValued; + } + @Override public List createPostProcessors(String variableName, int i) { List extractors = new ArrayList<>(); - JMESPathExtractor extractor = new JMESPathExtractor(); - extractor.setProperty(TestElement.GUI_CLASS, JMESPathExtractorGui.class.getName()); - extractor.setName("JSON Extractor (" + variableName + ")"); - extractor.setRefName(variableName); - extractor.setJmesPathExpression(path); - extractor.setMatchNumber(String.valueOf(matchNr)); - extractor.setDefaultValue(variableName + "_NOT_FOUND"); + AbstractTestElement extractor = createPostProcessor(variableName, i); extractors.add(extractor); return extractors; } @@ -170,13 +305,13 @@ public boolean equals(Object o) { } JsonCorrelationExtractor that = (JsonCorrelationExtractor) o; return matchNr == that.matchNr - && Objects.equals(path, that.path) + && Objects.equals(jsonpath, that.jsonpath) && Objects.equals(variableName, that.variableName) && target == that.target; } @Override public int hashCode() { - return Objects.hash(path, matchNr, variableName, target); + return Objects.hash(jsonpath, matchNr, variableName, target); } } diff --git a/src/main/java/com/blazemeter/jmeter/correlation/core/extractors/RegexCorrelationExtractor.java b/src/main/java/com/blazemeter/jmeter/correlation/core/extractors/RegexCorrelationExtractor.java index 417d670..aa90b12 100644 --- a/src/main/java/com/blazemeter/jmeter/correlation/core/extractors/RegexCorrelationExtractor.java +++ b/src/main/java/com/blazemeter/jmeter/correlation/core/extractors/RegexCorrelationExtractor.java @@ -12,13 +12,8 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; -import java.util.HashSet; import java.util.List; -import java.util.Map.Entry; import java.util.Objects; -import java.util.Set; -import java.util.function.Function; -import java.util.regex.Pattern; import org.apache.jmeter.extractor.RegexExtractor; import org.apache.jmeter.extractor.gui.RegexExtractorGui; import org.apache.jmeter.protocol.http.sampler.HTTPSamplerBase; @@ -50,12 +45,10 @@ public class RegexCorrelationExtractor extends protected static final String DEFAULT_MATCH_NUMBER_NAME = "match number"; protected static final String MULTIVALUED_DESCRIPTION = "Multivalued"; protected static final boolean DEFAULT_MULTIVALUED = false; - private static final Function VARIABLE_PATTERN_PROVIDER = - (variableName) -> Pattern.compile(variableName + "_(\\d|matchNr)"); + private static final Logger LOG = LoggerFactory.getLogger(RegexCorrelationExtractor.class); private static final String REGEX_EXTRACTOR_GUI_CLASS = RegexExtractorGui.class.getName(); private static final int DEFAULT_MATCH_NUMBER = 1; - private static final String DEFAULT_REGEX_EXTRACTOR_SUFFIX = "_NOT_FOUND"; protected boolean multiValued; protected String regex; protected int matchNr; @@ -250,15 +243,6 @@ public void process(HTTPSamplerBase sampler, List children, SampleR } - private void clearJMeterVariables(JMeterVariables vars) { - Set> entries = new HashSet<>(vars.entrySet()); - entries.forEach(e -> { - if (VARIABLE_PATTERN_PROVIDER.apply(variableName).matcher(e.getKey()).matches()) { - vars.remove(e.getKey()); - } - }); - } - private void addVarAndChildPostProcessor(String match, String variableName, RegexExtractor postProcessor) { if (AnalysisReporter.canCorrelate()) { @@ -287,7 +271,7 @@ public RegexExtractor createPostProcessor(String varName, int matchNr) { regexExtractor.setRegex(regex); regexExtractor.setTemplate("$" + groupNr + "$"); regexExtractor.setMatchNumber(matchNr); - regexExtractor.setDefaultValue(varName + DEFAULT_REGEX_EXTRACTOR_SUFFIX); + regexExtractor.setDefaultValue(varName + DEFAULT_EXTRACTOR_SUFFIX); regexExtractor.setUseField(target.getCode()); regexExtractor.setScopeAll(); return regexExtractor; @@ -302,16 +286,6 @@ public void update(CorrelationRuleTestElement testElem) { multiValued = isMultiValued(testElem); } - /** - * Used to provide information about the extraction of values from the response. - * This method would be used, when we are performing an analysis, regardless - * of the mode of the recording (i.e. whether we are recording or doing - * static analysis). - */ - public void analyze(String value, Object affectedElement, String varName) { - AnalysisReporter.report(this, value, affectedElement, varName, target.name()); - } - /** * Used to add the necessary elements to the sampler, to perform the * extraction of values from the response. @@ -368,7 +342,7 @@ public List createPostProcessors(String variableName, int i regexExtractor.setRegex(regex); regexExtractor.setTemplate("$" + groupNr + "$"); regexExtractor.setMatchNumber(matchNr); - regexExtractor.setDefaultValue(variableName + DEFAULT_REGEX_EXTRACTOR_SUFFIX); + regexExtractor.setDefaultValue(variableName + DEFAULT_EXTRACTOR_SUFFIX); regexExtractor.setUseField(target.getCode()); regexExtractor.setScopeAll(); return Collections.singletonList(regexExtractor); diff --git a/src/main/java/com/blazemeter/jmeter/correlation/core/replacements/CorrelationReplacement.java b/src/main/java/com/blazemeter/jmeter/correlation/core/replacements/CorrelationReplacement.java index 2d3d639..c0dce39 100644 --- a/src/main/java/com/blazemeter/jmeter/correlation/core/replacements/CorrelationReplacement.java +++ b/src/main/java/com/blazemeter/jmeter/correlation/core/replacements/CorrelationReplacement.java @@ -12,8 +12,10 @@ import java.util.Collection; import java.util.LinkedList; import java.util.List; +import java.util.function.Function; import org.apache.jmeter.config.Argument; import org.apache.jmeter.config.ConfigTestElement; +import org.apache.jmeter.engine.util.CompoundVariable; import org.apache.jmeter.protocol.http.control.Header; import org.apache.jmeter.protocol.http.sampler.HTTPSamplerBase; import org.apache.jmeter.samplers.SampleResult; @@ -48,10 +50,29 @@ public abstract class CorrelationReplacement exten protected static final String PROPERTIES_PREFIX = "CorrelationRule.CorrelationReplacement."; + public static final String REPLACEMENT_STRING_PROPERTY_NAME = PROPERTIES_PREFIX + + "replacementString"; + public static final String REPLACEMENT_IGNORE_VALUE_PROPERTY_NAME = PROPERTIES_PREFIX + + "ignoreValue"; protected static final String REGEX_DEFAULT_VALUE = "param=\"(.+?)\""; + + protected static final String REPLACEMENT_STRING_DEFAULT_VALUE = ""; + + protected static final String FUNCTION_REF_PREFIX = "${"; //$NON-NLS-1$ + /** + * Functions are wrapped in ${ and }. + */ + protected static final String FUNCTION_REF_SUFFIX = "}"; //$NON-NLS-1$ + private static final Logger LOG = LoggerFactory.getLogger(CorrelationReplacement.class); + protected String variableName; + protected String replacementString = REPLACEMENT_STRING_DEFAULT_VALUE; + + protected Function expressionEvaluator = + (expression) -> new CompoundVariable(expression).execute(); + /** * Default constructor added in order to satisfy the JSON conversion. @@ -243,4 +264,26 @@ public String toString() { * @param ruleTestElement CorrelationRuleTestElement that contains the values */ public abstract void update(CorrelationRuleTestElement ruleTestElement); + + Function replaceExpressionProvider() { + return s -> replacementString == null + || !java.util.regex.Pattern.compile("(\\$\\{.+?})").matcher(replacementString).matches() + || replacementString.isEmpty() + ? FUNCTION_REF_PREFIX + s + FUNCTION_REF_SUFFIX : s; + } + + String computeStringReplacement(String varName) { + String rawReplacementString = buildReplacementStringForMultivalued(varName); + String computed = expressionEvaluator.apply(rawReplacementString); + LOG.debug("Result of {} was {}", rawReplacementString, computed); + return computed; + } + + String buildReplacementStringForMultivalued(String varNameMatch) { + if (replacementString != null && replacementString.contains(variableName)) { + return replacementString.replace(variableName, varNameMatch); + } + return replacementString; + } + } diff --git a/src/main/java/com/blazemeter/jmeter/correlation/core/replacements/JsonCorrelationReplacement.java b/src/main/java/com/blazemeter/jmeter/correlation/core/replacements/JsonCorrelationReplacement.java new file mode 100644 index 0000000..59f66a9 --- /dev/null +++ b/src/main/java/com/blazemeter/jmeter/correlation/core/replacements/JsonCorrelationReplacement.java @@ -0,0 +1,323 @@ +package com.blazemeter.jmeter.correlation.core.replacements; + +import static com.blazemeter.jmeter.correlation.core.automatic.JMeterElementUtils.classIsNumberOrBoolean; +import static com.blazemeter.jmeter.correlation.core.automatic.JMeterElementUtils.jsonFindMatches; + +import com.blazemeter.jmeter.correlation.core.BaseCorrelationContext; +import com.blazemeter.jmeter.correlation.core.CorrelationContext; +import com.blazemeter.jmeter.correlation.core.ParameterDefinition; +import com.blazemeter.jmeter.correlation.core.analysis.AnalysisReporter; +import com.blazemeter.jmeter.correlation.core.automatic.JMeterElementUtils; +import com.blazemeter.jmeter.correlation.gui.CorrelationRuleTestElement; +import com.google.common.annotations.VisibleForTesting; +import com.jayway.jsonpath.InvalidPathException; +import com.jayway.jsonpath.JsonPath; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.function.Function; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import net.minidev.json.JSONArray; +import org.apache.commons.lang3.tuple.Pair; +import org.apache.jmeter.protocol.http.sampler.HTTPSamplerBase; +import org.apache.jmeter.samplers.SampleResult; +import org.apache.jmeter.testelement.TestElement; +import org.apache.jmeter.threads.JMeterVariables; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Correlation Replacements that applies the replacement using Json Xpath and the captured + * values. + * + * @param correlation context that can be used to store and share values during replay + */ + +public class JsonCorrelationReplacement extends + CorrelationReplacement { + + protected static final String JSONPATH_DEFAULT_VALUE = "$.jsonpath.expression"; + protected static final String REPLACEMENT_JSON_PROPERTY_NAME = PROPERTIES_PREFIX + "jsonpath"; + protected static final String REPLACEMENT_JSON_PROPERTY_DESCRIPTION = + "JSONPath expression"; + + private static final Logger LOG = LoggerFactory.getLogger(RegexCorrelationReplacement.class); + private static final boolean IGNORE_VALUE_DEFAULT = false; + private static final String REPLACEMENT_STRING_DEFAULT_VALUE = ""; + + private static final String ESCAPE_QUOTE_LEFT = "_CR_L_"; + private static final String ESCAPE_QUOTE_RIGHT = "_CR_R_"; + + protected String jsonpath = JSONPATH_DEFAULT_VALUE; + protected boolean ignoreValue = IGNORE_VALUE_DEFAULT; + + private Object currentSampler; + + public JsonCorrelationReplacement() { + } + + public JsonCorrelationReplacement(String jsonpath) { + this.jsonpath = jsonpath; + } + + public JsonCorrelationReplacement(String jsonpath, String replacementString, String ignoreValue) { + this.jsonpath = jsonpath; + this.replacementString = replacementString; + this.ignoreValue = Boolean.parseBoolean(ignoreValue); + } + + @Override + public String getDisplayName() { + return "JSON"; + } + + @Override + public List getParams() { + return Arrays.asList(jsonpath, replacementString, Boolean.toString(ignoreValue)); + } + + @Override + public void setParams(List params) { + jsonpath = !params.isEmpty() ? params.get(0) : JSONPATH_DEFAULT_VALUE; + replacementString = params.size() > 1 ? params.get(1) : REPLACEMENT_STRING_DEFAULT_VALUE; + ignoreValue = params.size() > 2 ? Boolean.parseBoolean(params.get(2)) : IGNORE_VALUE_DEFAULT; + } + + @Override + public List getParamsDefinition() { + return Arrays.asList( + new ParameterDefinition.TextParameterDefinition(REPLACEMENT_JSON_PROPERTY_NAME, + REPLACEMENT_JSON_PROPERTY_DESCRIPTION, JSONPATH_DEFAULT_VALUE), + new ParameterDefinition.TextParameterDefinition(REPLACEMENT_STRING_PROPERTY_NAME, + "Replacement string", + REPLACEMENT_STRING_DEFAULT_VALUE, true), + new ParameterDefinition.CheckBoxParameterDefinition(REPLACEMENT_IGNORE_VALUE_PROPERTY_NAME, + "Ignore Value", + IGNORE_VALUE_DEFAULT, true)); + } + + @Override + protected String replaceString(String input, JMeterVariables vars) { + // https://github.com/json-path/JsonPath?tab=readme-ov-file#set-a-value + // Skip empty inputs + if (input == null || input.isEmpty() || jsonpath == null || jsonpath.isEmpty() + || variableName == null + || variableName.isEmpty()) { + return input; + } + // For previous replaced matches with variables, escape the unquoted variables + String inputProcessed = escapeUnquotedVariablesWithMarks(input); + if (JMeterElementUtils.isJson(inputProcessed)) { + HashSet> valuesReplaced = new HashSet(); + + // Test if the path of the jsonpath match + Pair> result = + jsonFindMatches(inputProcessed, jsonpath); + String updatedInput = inputProcessed; + Class resultType = result.getLeft(); + ArrayList matches = result.getRight(); + if (matches.size() > 0) { + // Ok, match, try to each match get the path and replace + Function expressionProvider = replaceExpressionProvider(); + String currentVariableName = variableName; + for (int varNr = 0; varNr < matches.size(); varNr++) { + String valueStr = matches.get(varNr); + String varMatched = searchVariable(vars, valueStr); + currentVariableName = varMatched; + String replaceExpression = null; + // When ignore value, use the replacement string + if (!replacementString.isEmpty() && ignoreValue) { + replaceExpression = replacementString; + } else if (varMatched != null && replacementString.isEmpty()) { + // When no replacement string + replaceExpression = expressionProvider.apply(varMatched); + } else if (varMatched != null) { // When replacement string, use replacement string + replaceExpression = expressionProvider.apply( + buildReplacementStringForMultivalued(varMatched)); + } + if (replaceExpression != null) { + boolean inArray = resultType == JSONArray.class; + String updatedJsonPath = inArray ? jsonpath + "[" + varNr + "]" : jsonpath; + Pair> toUpdateMatches = + jsonFindMatches(updatedInput, updatedJsonPath); + if (toUpdateMatches.getRight() != null) { + Class updateResultType = toUpdateMatches.getLeft(); + boolean originIsUnQuoted = classIsNumberOrBoolean(updateResultType); + if (originIsUnQuoted) { + // When value is needed to put in the json structure without the quotes + // this not is allowed by jayway because generate an invalid json with free text + // inside, we need to post process to remove the left and the right marks to o that + // Remember, jayway put the value as a quoted String, + // and is why we need to put marks to recover the format without quotes at the end. + replaceExpression = ESCAPE_QUOTE_LEFT + replaceExpression + ESCAPE_QUOTE_RIGHT; + } + try { + updatedInput = + JsonPath.parse(updatedInput).set(updatedJsonPath, replaceExpression) + .jsonString(); + // Store the values matched and used in the replacement + valuesReplaced.add(Pair.of(valueStr, variableName)); + } catch (InvalidPathException e) { + LOG.debug( + "JSONPath used to update target value doesn't match in the set: " + + "value:{} jsonpath={}", + valueStr, updatedJsonPath); + } + } else { + LOG.debug( + "JSONPath used to update target value doesn't match in the get: " + + "value:{} jsonpath={}", + valueStr, updatedJsonPath); + } + } + } + // The json path match, replace the value with the replacement variable + if (updatedInput != null && !updatedInput.equals(inputProcessed)) { + for (Pair valueReplaced : valuesReplaced) { + analysis(valueReplaced.getLeft(), valueReplaced.getRight()); + } + + // Replace the start and end marks used for the values without quotes + // This is needed to recover the original format + updatedInput = unescapeQuotedVariablesWithMarks(updatedInput); + + if (AnalysisReporter.canCorrelate()) { + return updatedInput; + } else { + return input; + } + + } + } + } + return input; // When none of previous logic generate a return, return default input + } + + private void analysis(String literalMatched, String currentVariableName) { + AnalysisReporter.report(this, literalMatched, currentSampler, currentVariableName); + } + + private String searchVariable(JMeterVariables vars, String value) { + int varNr = 0; + while (varNr <= context.getVariableCount(variableName)) { + String varName = varNr == 0 ? variableName : variableName + "#" + varNr; + String varMatchesCount = vars.get(varName + "_matchNr"); + int matchNr = varMatchesCount == null ? 0 : Integer.parseInt(varMatchesCount); + if (matchNr == 0) { + if (vars.get(varName) != null && vars.get(varName).equals(value)) { + return varName; + } + } + int varMatch = 1; + while (varMatch <= matchNr) { + String varNameMatch = varName + "_" + varMatch; + if (vars.get(varNameMatch) != null && vars.get(varNameMatch).equals(value)) { + return varNameMatch; + } + varMatch += 1; + } + varNr++; + } + return null; + } + + private String escapeUnquotedVariablesWithMarks(String json) { + // Try to escape unquoted variable/function in json + // this try to allow a json parse without error for json path evaluation + + String regex = "(\\$\\{[^}]+\\})(?=(?:[^\"\\}]*\\})|(?:[^\"\\]]*\\]))"; + + Pattern pattern = Pattern.compile(regex); + Matcher matcher = pattern.matcher(json); + + StringBuffer result = new StringBuffer(); + // Search the variable/function and escape with the particular pre-fix / sub-fix + // the usage of this format allow to recover the original format + while (matcher.find()) { + matcher.appendReplacement(result, + "\"" + ESCAPE_QUOTE_LEFT + "$1" + ESCAPE_QUOTE_RIGHT + "\""); + } + matcher.appendTail(result); + return result.toString(); + } + + private String unescapeQuotedVariablesWithMarks(String json) { + // Recover the format, values quoted with special marks + // are transformed to unquote value + return json.replace( + "\"" + ESCAPE_QUOTE_LEFT, + "").replace( + ESCAPE_QUOTE_RIGHT + "\"", ""); + } + + String buildReplacementStringForMultivalued(String varNameMatch) { + if (replacementString != null && replacementString.contains(variableName)) { + return replacementString.replace(variableName, varNameMatch); + } + return replacementString; + } + + @Override + public Class getSupportedContext() { + return BaseCorrelationContext.class; + } + + @Override + public void updateTestElem(CorrelationRuleTestElement testElem) { + super.updateTestElem(testElem); + testElem.setProperty(REPLACEMENT_JSON_PROPERTY_NAME, jsonpath); + testElem.setProperty(REPLACEMENT_STRING_PROPERTY_NAME, replacementString); + testElem.setProperty(REPLACEMENT_IGNORE_VALUE_PROPERTY_NAME, ignoreValue); + } + + @Override + public void process(HTTPSamplerBase sampler, List children, SampleResult result, + JMeterVariables vars) { + if (jsonpath.isEmpty()) { + return; + } + currentSampler = sampler; + super.process(sampler, children, result, vars); + } + + @Override + public void update(CorrelationRuleTestElement testElem) { + jsonpath = testElem.getPropertyAsString(REPLACEMENT_JSON_PROPERTY_NAME); + replacementString = testElem.getPropertyAsString(REPLACEMENT_STRING_PROPERTY_NAME); + ignoreValue = testElem.getPropertyAsBoolean(REPLACEMENT_IGNORE_VALUE_PROPERTY_NAME); + } + + @Override + public String toString() { + return "RegexCorrelationReplacement{" + + ", jsonpath='" + jsonpath + "'" + + ", replacementString='" + replacementString + "'" + + ", ignoreValue=" + ignoreValue + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + JsonCorrelationReplacement that = (JsonCorrelationReplacement) o; + return Objects.equals(jsonpath, that.jsonpath); + } + + @Override + public int hashCode() { + return Objects.hash(jsonpath); + } + + @VisibleForTesting + public void setExpressionEvaluator(Function expressionEvaluator) { + this.expressionEvaluator = expressionEvaluator; + } +} diff --git a/src/main/java/com/blazemeter/jmeter/correlation/core/replacements/RegexCorrelationReplacement.java b/src/main/java/com/blazemeter/jmeter/correlation/core/replacements/RegexCorrelationReplacement.java index 8a90785..b99ae2d 100644 --- a/src/main/java/com/blazemeter/jmeter/correlation/core/replacements/RegexCorrelationReplacement.java +++ b/src/main/java/com/blazemeter/jmeter/correlation/core/replacements/RegexCorrelationReplacement.java @@ -9,11 +9,12 @@ import com.blazemeter.jmeter.correlation.gui.CorrelationRuleTestElement; import com.google.common.annotations.VisibleForTesting; import java.util.Arrays; +import java.util.HashSet; import java.util.List; import java.util.Objects; import java.util.function.Function; import java.util.function.Predicate; -import org.apache.jmeter.engine.util.CompoundVariable; +import org.apache.commons.lang3.tuple.Pair; import org.apache.jmeter.protocol.http.sampler.HTTPSamplerBase; import org.apache.jmeter.samplers.SampleResult; import org.apache.jmeter.testelement.TestElement; @@ -37,28 +38,16 @@ public class RegexCorrelationReplacement extends CorrelationReplacement { - public static final String REPLACEMENT_STRING_PROPERTY_NAME = PROPERTIES_PREFIX + - "replacementString"; - public static final String REPLACEMENT_IGNORE_VALUE_PROPERTY_NAME = PROPERTIES_PREFIX + - "ignoreValue"; protected static final String REPLACEMENT_REGEX_PROPERTY_NAME = PROPERTIES_PREFIX + "regex"; protected static final String REPLACEMENT_REGEX_PROPERTY_DESCRIPTION = "Regular expression " + "replacement"; - protected static final String FUNCTION_REF_PREFIX = "${"; //$NON-NLS-1$ - /** - * Functions are wrapped in ${ and }. - */ - protected static final String FUNCTION_REF_SUFFIX = "}"; //$NON-NLS-1$ private static final Logger LOG = LoggerFactory.getLogger(RegexCorrelationReplacement.class); private static final boolean IGNORE_VALUE_DEFAULT = false; - private static final String REPLACEMENT_STRING_DEFAULT_VALUE = ""; + protected String regex = REGEX_DEFAULT_VALUE; protected boolean ignoreValue = IGNORE_VALUE_DEFAULT; - protected String replacementString = REPLACEMENT_STRING_DEFAULT_VALUE; - private Function expressionEvaluator = - (expression) -> new CompoundVariable(expression).execute(); + private Object currentSampler; - private String currentVariableName = ""; /** * Default constructor added in order to satisfy the JSON conversion. @@ -197,6 +186,7 @@ protected String replaceWithRegex(String input, String regex, LOG.warn("Malformed pattern: {}", regex, e); throw e; } + HashSet> valuesReplaced = new HashSet(); PatternMatcherInput patternMatcherInput = new PatternMatcherInput(input); int beginOffset = patternMatcherInput.getBeginOffset(); @@ -215,6 +205,7 @@ protected String replaceWithRegex(String input, String regex, String varName = varNr == 0 ? variableName : variableName + "#" + varNr; String varMatchesCount = vars.get(varName + "_matchNr"); literalMatched = match.group(1); + String currentVariableName = ""; String replaceExpression = null; if (varMatchesCount == null) { if (vars.get(varName) != null && vars.get(varName).equals(literalMatched) @@ -237,6 +228,7 @@ protected String replaceWithRegex(String input, String regex, if (replaceExpression != null) { result = replaceMatch(result, patternMatcherInput, match, beginOffset, inputBuffer, replaceExpression); + valuesReplaced.add(Pair.of(literalMatched, currentVariableName)); } } else { int matchNr = Integer.parseInt(varMatchesCount); @@ -257,6 +249,8 @@ protected String replaceWithRegex(String input, String regex, if (replaceExpression != null) { result = replaceMatch(result, patternMatcherInput, match, beginOffset, inputBuffer, expressionProvider.apply(replaceExpression)); + + valuesReplaced.add(Pair.of(literalMatched, currentVariableName)); } varMatch++; } @@ -275,7 +269,9 @@ protected String replaceWithRegex(String input, String regex, return input; } - analysis(literalMatched); + for (Pair valueReplaced : valuesReplaced) { + analysis(valueReplaced.getLeft(), valueReplaced.getRight()); + } if (!AnalysisReporter.canCorrelate()) { return input; } @@ -283,27 +279,6 @@ protected String replaceWithRegex(String input, String regex, return replacedInput; } - private Function replaceExpressionProvider() { - return s -> replacementString == null - || !java.util.regex.Pattern.compile("(\\$\\{.+?})").matcher(replacementString).matches() - || replacementString.isEmpty() - ? FUNCTION_REF_PREFIX + s + FUNCTION_REF_SUFFIX : s; - } - - private String computeStringReplacement(String varName) { - String rawReplacementString = buildReplacementStringForMultivalued(varName); - String computed = expressionEvaluator.apply(rawReplacementString); - LOG.debug("Result of {} was {}", rawReplacementString, computed); - return computed; - } - - private String buildReplacementStringForMultivalued(String varNameMatch) { - if (replacementString != null && replacementString.contains(variableName)) { - return replacementString.replace(variableName, varNameMatch); - } - return replacementString; - } - private StringBuilder replaceMatch(StringBuilder result, PatternMatcherInput patternMatcherInput, MatchResult match, int beginOffset, char[] inputBuffer, String expression) { @@ -358,7 +333,7 @@ public void update(CorrelationRuleTestElement testElem) { ignoreValue = testElem.getPropertyAsBoolean(REPLACEMENT_IGNORE_VALUE_PROPERTY_NAME); } - private void analysis(String literalMatched) { + private void analysis(String literalMatched, String currentVariableName) { AnalysisReporter.report(this, literalMatched, currentSampler, currentVariableName); } diff --git a/src/main/java/com/blazemeter/jmeter/correlation/core/suggestions/method/ComparisonMethod.java b/src/main/java/com/blazemeter/jmeter/correlation/core/suggestions/method/ComparisonMethod.java index b2e404b..583834f 100644 --- a/src/main/java/com/blazemeter/jmeter/correlation/core/suggestions/method/ComparisonMethod.java +++ b/src/main/java/com/blazemeter/jmeter/correlation/core/suggestions/method/ComparisonMethod.java @@ -88,7 +88,7 @@ private static boolean hasOrphans(CorrelationSuggestion suggestion) { */ private static ExtractionSuggestion generateCandidateExtractor(SampleResult result, Appearances appearance, - CorrelationExtractor extractor, + CorrelationExtractor extractor, StructureType structureType, String name) { ExtractionSuggestion suggestion = new ExtractionSuggestion(extractor, result); diff --git a/src/main/java/com/blazemeter/jmeter/correlation/gui/CorrelationComponentsRegistry.java b/src/main/java/com/blazemeter/jmeter/correlation/gui/CorrelationComponentsRegistry.java index 5becbae..51e795c 100644 --- a/src/main/java/com/blazemeter/jmeter/correlation/gui/CorrelationComponentsRegistry.java +++ b/src/main/java/com/blazemeter/jmeter/correlation/gui/CorrelationComponentsRegistry.java @@ -10,9 +10,9 @@ import com.blazemeter.jmeter.correlation.core.extractors.CorrelationExtractor; import com.blazemeter.jmeter.correlation.core.extractors.JsonCorrelationExtractor; import com.blazemeter.jmeter.correlation.core.extractors.RegexCorrelationExtractor; -import com.blazemeter.jmeter.correlation.core.extractors.XmlCorrelationExtractor; import com.blazemeter.jmeter.correlation.core.replacements.CorrelationReplacement; import com.blazemeter.jmeter.correlation.core.replacements.FunctionCorrelationReplacement; +import com.blazemeter.jmeter.correlation.core.replacements.JsonCorrelationReplacement; import com.blazemeter.jmeter.correlation.core.replacements.RegexCorrelationReplacement; import com.google.common.annotations.VisibleForTesting; import java.io.IOException; @@ -45,10 +45,10 @@ public class CorrelationComponentsRegistry { private final Set> customExtractors = new HashSet<>(); private final Set> customReplacements = new HashSet<>(); private final List> defaultExtractors = - Arrays.asList(RegexCorrelationExtractor.class, JsonCorrelationExtractor.class, - XmlCorrelationExtractor.class); - private final List> defaultReplacements = Collections - .singletonList(RegexCorrelationReplacement.class); + Arrays.asList(RegexCorrelationExtractor.class, JsonCorrelationExtractor.class); + private final List> defaultReplacements = + Arrays.asList(RegexCorrelationReplacement.class, + JsonCorrelationReplacement.class); private final List deprecatedComponents = Collections.singletonList( FunctionCorrelationReplacement.class.getCanonicalName()); diff --git a/src/main/java/com/blazemeter/jmeter/correlation/gui/CorrelationRulePartPanel.java b/src/main/java/com/blazemeter/jmeter/correlation/gui/CorrelationRulePartPanel.java index 987b807..5bee301 100644 --- a/src/main/java/com/blazemeter/jmeter/correlation/gui/CorrelationRulePartPanel.java +++ b/src/main/java/com/blazemeter/jmeter/correlation/gui/CorrelationRulePartPanel.java @@ -4,6 +4,7 @@ import com.blazemeter.jmeter.correlation.core.CorrelationRulePartTestElement; import com.blazemeter.jmeter.correlation.core.ParameterDefinition; import com.blazemeter.jmeter.correlation.gui.common.HelperDialog; +import com.blazemeter.jmeter.correlation.gui.common.RulePartType; import com.blazemeter.jmeter.correlation.gui.common.ThemedIcon; import com.blazemeter.jmeter.correlation.gui.common.ThemedIconLabel; import com.google.common.annotations.VisibleForTesting; @@ -36,6 +37,7 @@ import javax.swing.JTextField; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; +import org.apache.jorphan.gui.ComponentUtil; public class CorrelationRulePartPanel extends JPanel { @@ -54,7 +56,6 @@ public class CorrelationRulePartPanel extends JPanel { private final JLabel helper = new ThemedIconLabel("help.png"); private final List listAdvancedComponents = new ArrayList<>(); private HelperDialog helperDialog; - private String description = ""; private Runnable fieldsListener; private JPanel advancedPanel; private JLabel collapsibleIcon; @@ -150,7 +151,6 @@ private void changeEvent() { } removeComponents(); - this.description = getSelectedItem().getDescription(); getSelectedItem().getParamsDefinition().forEach(p -> { Component field = buildField(p); if (p.isAdvanced()) { @@ -252,8 +252,12 @@ private void prepareHelper(String name) { @Override public void mouseClicked(MouseEvent mouseEvent) { helperDialog = new HelperDialog(CorrelationRulePartPanel.this); - helperDialog.setTitle("Selector Information"); - helperDialog.updateDialogContent(description); + CorrelationRulePartTestElement selectedItem = getSelectedItem(); + String helperTitle = + selectedItem.getDisplayName() + " " + RulePartType.fromComponent(selectedItem); + helperDialog.setTitle(helperTitle); + helperDialog.updateDialogContent(selectedItem.getDescription()); + ComponentUtil.centerComponentInWindow(helperDialog); helperDialog.setVisible(true); } }); diff --git a/src/main/java/com/blazemeter/jmeter/correlation/gui/analysis/CorrelationTemplatesSelectionPanel.java b/src/main/java/com/blazemeter/jmeter/correlation/gui/analysis/CorrelationTemplatesSelectionPanel.java index e51bec0..26682b7 100644 --- a/src/main/java/com/blazemeter/jmeter/correlation/gui/analysis/CorrelationTemplatesSelectionPanel.java +++ b/src/main/java/com/blazemeter/jmeter/correlation/gui/analysis/CorrelationTemplatesSelectionPanel.java @@ -183,7 +183,7 @@ private void onTemplateVersionFocus(Template focusedVersion) { informationPane.setText(TemplateVersionUtils .getInformationAsHTLM(focusedVersion, false, canUse, - model.getRepositoryDisplayName(focusedVersion.getRepositoryId()))); + model.getRepositoryDisplayName(focusedVersion.getRepositoryId()))); informationPane.setCaretPosition(0); // Scroll to the top } @@ -342,9 +342,15 @@ private void onContinue() { = repManager.getTemplatesAndProperties(templates); if (templatesAndProperties == null || templatesAndProperties.isEmpty()) { - + // Get all the templates and properties for the local repository and filter the selected templatesAndProperties = config - .getCorrelationTemplatesAndPropertiesByRepositoryName(repositoryName, true); + .getCorrelationTemplatesAndPropertiesByRepositoryName(repositoryName, true) + .entrySet() + .stream() + .filter(templateEntry -> templates.stream().anyMatch(t -> + templateEntry.getKey().getId().equals(t.getName()) && + templateEntry.getKey().getVersion().equals(t.getVersion()))) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); } for (Map.Entry templateEntry diff --git a/src/main/java/com/blazemeter/jmeter/correlation/gui/automatic/CorrelationHistoryFrame.java b/src/main/java/com/blazemeter/jmeter/correlation/gui/automatic/CorrelationHistoryFrame.java index 1bfecad..e5bc46c 100644 --- a/src/main/java/com/blazemeter/jmeter/correlation/gui/automatic/CorrelationHistoryFrame.java +++ b/src/main/java/com/blazemeter/jmeter/correlation/gui/automatic/CorrelationHistoryFrame.java @@ -61,6 +61,8 @@ public class CorrelationHistoryFrame extends JDialog implements ActionListener { private static final String DELETE = "delete"; private static final String RESTORE = "restore"; private static final String ZIP = "zip"; + private static final String CREATE = "create"; + protected CorrelationHistory history; protected JDialog runDialog; @@ -69,6 +71,7 @@ public class CorrelationHistoryFrame extends JDialog implements ActionListener { private JButton deleteButton; private JButton restoreButton; private JButton zipButton; + private JButton createCheckpointButton; public CorrelationHistoryFrame(CorrelationHistory history) { super(); @@ -117,6 +120,9 @@ private JPanel makeCorrelationHistoryPanel() { TableColumn descColumn = table.getColumnModel().getColumn(2); descColumn.setMinWidth(400); + TableColumn notesColumn = table.getColumnModel().getColumn(3); + notesColumn.setMinWidth(150); + JTableHeader header = table.getTableHeader(); header.setFont(header.getFont().deriveFont(14f)); @@ -143,12 +149,20 @@ private JPanel makeCorrelationHistoryPanel() { .withToolTip("Export history to a zip file.") .build(); + createCheckpointButton = builder.withAction(CREATE) + .withName("createIteration") + .withText("Create Checkpoint") + .withToolTip("Create a snapshot of the jmx state as history iteration.") + .build(); + JPanel buttonsPanel = new JPanel(); buttonsPanel.add(deleteButton); buttonsPanel.add(Box.createRigidArea(new Dimension(10, 0))); buttonsPanel.add(restoreButton); buttonsPanel.add(Box.createRigidArea(new Dimension(10, 0))); buttonsPanel.add(zipButton); + buttonsPanel.add(Box.createRigidArea(new Dimension(10, 0))); + buttonsPanel.add(createCheckpointButton); JPanel displayTablePanel = new JPanel(); displayTablePanel.setLayout(new GridBagLayout()); @@ -226,6 +240,10 @@ public void actionPerformed(ActionEvent e) { case ZIP: this.zipHistory(); return; + case CREATE: + this.history.addCustomIteration(); + this.loadSteps(history.getSteps()); + return; default: LOG.warn("Action {} not supported", action); } @@ -257,7 +275,7 @@ public void zipHistory() { } public static class HistoryTableModel extends DefaultTableModel { - private final List columns = Arrays.asList("", "Timestamp", "Description"); + private final List columns = Arrays.asList("", "Timestamp", "Description", "Notes"); private final List stepList = new ArrayList<>(); private final Map> suggestionsMap = new HashMap<>(); @@ -310,6 +328,8 @@ public Object getValueAt(int rowIndex, int columnIndex) { return "Not available"; case 2: return step.getStepMessage(); + case 3: + return step.getNotes(); default: return "N/A"; } @@ -319,12 +339,19 @@ public Object getValueAt(int rowIndex, int columnIndex) { public void setValueAt(Object aValue, int rowIndex, int columnIndex) { if (!stepList.isEmpty() && columnIndex == 0) { stepList.get(rowIndex).setSelected((boolean) aValue); + } else if (columnIndex == 1) { + stepList.get(rowIndex).getStep().setTimestamp((String) aValue); + } else if (columnIndex == 2) { + stepList.get(rowIndex).getStep().setStepMessage((String) aValue); + } else if (columnIndex == 3) { + stepList.get(rowIndex).getStep().setNotes((String) aValue); } + } @Override public boolean isCellEditable(int rowIndex, int columnIndex) { - return columnIndex == 0; + return columnIndex != 1; } public List getSelectedSteps() { diff --git a/src/main/java/com/blazemeter/jmeter/correlation/gui/automatic/CorrelationSuggestionsPanel.java b/src/main/java/com/blazemeter/jmeter/correlation/gui/automatic/CorrelationSuggestionsPanel.java index cf02c4d..e38f139 100644 --- a/src/main/java/com/blazemeter/jmeter/correlation/gui/automatic/CorrelationSuggestionsPanel.java +++ b/src/main/java/com/blazemeter/jmeter/correlation/gui/automatic/CorrelationSuggestionsPanel.java @@ -362,8 +362,15 @@ private void exportSuggestions() { repManager.getTemplatesAndProperties(templates); if (templatesAndProperties == null || templatesAndProperties.isEmpty()) { + // Get all the templates and properties for the local repository and filter the selected templatesAndProperties = this.wizard.getRepositoriesConfiguration() - .getCorrelationTemplatesAndPropertiesByRepositoryName(repositoryName, true); + .getCorrelationTemplatesAndPropertiesByRepositoryName(repositoryName, true) + .entrySet() + .stream() + .filter(templateEntry -> templates.stream().anyMatch(t -> + templateEntry.getKey().getId().equals(t.getName()) && + templateEntry.getKey().getVersion().equals(t.getVersion()))) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); } for (Map.Entry templateEntry @@ -393,7 +400,7 @@ private void exportSuggestions() { for (CorrelationSuggestion suggestion : suggestions) { Template source = suggestion.getSource(); // Automatic or Template based - if (source == null || canExport.contains(source)) { + if (source == null || templateContains(canExport, source)) { rules.addAll(suggestion.toCorrelationRules()); } } @@ -406,6 +413,12 @@ private void exportSuggestions() { JOptionPane.INFORMATION_MESSAGE); } + private boolean templateContains(Set