diff --git a/config/xsl/loadreport/util/timer-row.xsl b/config/xsl/loadreport/util/timer-row.xsl index 4fb6f3fc5..66bc36905 100644 --- a/config/xsl/loadreport/util/timer-row.xsl +++ b/config/xsl/loadreport/util/timer-row.xsl @@ -43,6 +43,13 @@ + + + + + + + diff --git a/config/xsl/loadreport/util/timer-summary-row.xsl b/config/xsl/loadreport/util/timer-summary-row.xsl index 9abecf65a..b50aeafe8 100644 --- a/config/xsl/loadreport/util/timer-summary-row.xsl +++ b/config/xsl/loadreport/util/timer-summary-row.xsl @@ -11,6 +11,11 @@ + + + + + @@ -105,9 +110,8 @@ - - + + diff --git a/config/xsl/loadreport/util/timer-table.xsl b/config/xsl/loadreport/util/timer-table.xsl index 950312d8c..ff8eb4758 100644 --- a/config/xsl/loadreport/util/timer-table.xsl +++ b/config/xsl/loadreport/util/timer-table.xsl @@ -35,6 +35,9 @@ + + Label + Count @@ -148,13 +151,13 @@ - + - + - + diff --git a/doc/internal-doc/api.sig b/doc/internal-doc/api.sig index 114b22145..5e2ee4c63 100644 --- a/doc/internal-doc/api.sig +++ b/doc/internal-doc/api.sig @@ -1,5 +1,5 @@ #Signature file v4.1 -#Version 8.0.0 +#Version 8.3.0 CLSS public abstract com.xceptance.xlt.api.actions.AbstractAction cons protected init(com.xceptance.xlt.api.actions.AbstractAction,java.lang.String) @@ -278,17 +278,19 @@ intf com.xceptance.xlt.api.engine.Data meth public char getTypeCode() meth public final void setAllValues(java.util.List) meth public java.lang.String getAgentName() +meth public java.lang.String getLabel() meth public java.lang.String getName() meth public java.lang.String getTransactionName() meth public java.util.List toList() meth public long getTime() meth public void setAgentName(java.lang.String) meth public void setBaseValues(java.util.List) +meth public void setLabel(java.lang.String) meth public void setName(java.lang.String) meth public void setTime(long) meth public void setTransactionName(java.lang.String) supr java.lang.Object -hfds agentName,name,time,transactionName,typeCode +hfds agentName,label,name,time,transactionName,typeCode CLSS public com.xceptance.xlt.api.engine.ActionData cons public init() @@ -315,12 +317,14 @@ hfds TYPE_CODE,value CLSS public abstract interface com.xceptance.xlt.api.engine.Data meth public abstract char getTypeCode() meth public abstract java.lang.String getAgentName() +meth public abstract java.lang.String getLabel() meth public abstract java.lang.String getName() meth public abstract java.lang.String getTransactionName() meth public abstract java.util.List toList() meth public abstract long getTime() meth public abstract void setAgentName(java.lang.String) meth public abstract void setBaseValues(java.util.List) +meth public abstract void setLabel(java.lang.String) meth public abstract void setName(java.lang.String) meth public abstract void setRemainingValues(java.util.List) meth public abstract void setTime(long) diff --git a/samples/testsuite-posters/config/misc/reporting-labeling-rules.properties b/samples/testsuite-posters/config/misc/reporting-labeling-rules.properties new file mode 100644 index 000000000..beee7e51d --- /dev/null +++ b/samples/testsuite-posters/config/misc/reporting-labeling-rules.properties @@ -0,0 +1,107 @@ +################################################################################ +# +# Data Record Labeling Rules +# +# During report creation, data records may get a custom label. For example, all +# requests in the checkout area of a shopping app could be labeled with +# "checkout". Use these labels for advanced analysis with XLT or other tools. +# They will be part of both the HTML report and the XML data file. +# +# The automatic labeling of data records is controlled by "labeling rules". A +# rule knows how to select all data records of interest and how to process them. +# +# You may specify one or more rules as outlined below: +# +# com.xceptance.xlt.reportgenerator.labelingRules.. = +# +# The rules are sorted numerically based on and are applied in ascending +# order. +# +# The supported values for "" are: +# +# newLabel ................. The new label (required) +# +# namePattern [n] .......... Regex defining a matching record name +# labelPattern [l] ......... Regex defining a matching label +# typeCodePattern [t] ...... Regex defining a matching record type code +# +# stopOnMatch .............. Whether or not to process the next rule even if +# the current rule applied (defaults to true). +# +# If more than one pattern is given, all given patterns must match for the +# record to be processed. +# +# Note that newLabel may contain placeholders, which are replaced with the +# specified capturing group from the respective pattern. The placeholder +# format is as follows: {:}, where is +# the type code of the respective pattern (given in brackets above) and +# denotes the respective capturing group in the selected +# pattern. +# +# You may also use just {} as placeholder. Such placeholders do not +# require a pattern and are resolved to the full text of the respective record +# attribute. +# +# Excluding instead of Including +# +# com.xceptance.xlt.reportgenerator.labelingRules...exclude = +# +# All records that match the exclude pattern will not be selected. For example, +# to label all transaction data records with a name other than "TOrder" with +# "anonymous", you would setup a rule like that. +# +# com.xceptance.xlt.reportgenerator.labelingRules.1.newLabel = anonymous +# com.xceptance.xlt.reportgenerator.labelingRules.1.namePattern.exclude = TOrder +# com.xceptance.xlt.reportgenerator.labelingRules.1.typeCodePattern = T +# +# Please note that an include pattern as well as an exclude pattern can be specified for +# a pattern type at the same time. In this case, a record is selected if and only if it +# matches the include pattern, but does not match the exclude pattern. +# +################################################################################ + +### Request/Action Labeling ### + +## Homepage +com.xceptance.xlt.reportgenerator.labelingRules.1.newLabel = homepage +com.xceptance.xlt.reportgenerator.labelingRules.1.namePattern = ^Homepage +com.xceptance.xlt.reportgenerator.labelingRules.1.typeCodePattern = A|R + +## Catalog +com.xceptance.xlt.reportgenerator.labelingRules.2.newLabel = catalog +com.xceptance.xlt.reportgenerator.labelingRules.2.namePattern = ^(SelectTopCategory|SelectCategory|ProductDetailView) +com.xceptance.xlt.reportgenerator.labelingRules.2.typeCodePattern = A|R + +## Cart +com.xceptance.xlt.reportgenerator.labelingRules.3.newLabel = cart +com.xceptance.xlt.reportgenerator.labelingRules.3.namePattern = ^(AddToCart|ViewCart) +com.xceptance.xlt.reportgenerator.labelingRules.3.typeCodePattern = A|R + +## Checkout +com.xceptance.xlt.reportgenerator.labelingRules.4.newLabel = checkout +com.xceptance.xlt.reportgenerator.labelingRules.4.namePattern = ^(StartCheckout|EnterShippingAddress|EnterBillingAddress|EnterPaymentMethod) +com.xceptance.xlt.reportgenerator.labelingRules.4.typeCodePattern = A|R + +## Order +com.xceptance.xlt.reportgenerator.labelingRules.5.newLabel = order +com.xceptance.xlt.reportgenerator.labelingRules.5.namePattern = ^(PlaceOrder) +com.xceptance.xlt.reportgenerator.labelingRules.5.typeCodePattern = A|R + +## Account +com.xceptance.xlt.reportgenerator.labelingRules.6.newLabel = account +com.xceptance.xlt.reportgenerator.labelingRules.6.namePattern = ^(Login|Logout|Register|GoToRegistrationForm|GoToSignIn) +com.xceptance.xlt.reportgenerator.labelingRules.6.typeCodePattern = A|R + + +### Transaction Labeling ### + +## Registered +com.xceptance.xlt.reportgenerator.labelingRules.10.newLabel = registered +com.xceptance.xlt.reportgenerator.labelingRules.10.namePattern = ^TOrder +com.xceptance.xlt.reportgenerator.labelingRules.10.typeCodePattern = T + +## Anonymous +com.xceptance.xlt.reportgenerator.labelingRules.11.newLabel = anonymous +#com.xceptance.xlt.reportgenerator.labelingRules.11.namePattern = +com.xceptance.xlt.reportgenerator.labelingRules.11.typeCodePattern = T + diff --git a/src/main/java/com/xceptance/xlt/api/engine/AbstractData.java b/src/main/java/com/xceptance/xlt/api/engine/AbstractData.java index 6ba926257..cb76a33d5 100644 --- a/src/main/java/com/xceptance/xlt/api/engine/AbstractData.java +++ b/src/main/java/com/xceptance/xlt/api/engine/AbstractData.java @@ -56,6 +56,11 @@ public abstract class AbstractData implements Data */ private String agentName; + /** + * The custom label determined by the labeling rules. Only used during report generation or analysis. + */ + private String label; + /** * Creates a new AbstractData object and gives it the specified name and type code. * @@ -216,4 +221,20 @@ public List toList() return fields; } + + /** + * {@inheritDoc} + */ + public String getLabel() + { + return label; + } + + /** + * {@inheritDoc} + */ + public void setLabel(String label) + { + this.label = label; + } } diff --git a/src/main/java/com/xceptance/xlt/api/engine/Data.java b/src/main/java/com/xceptance/xlt/api/engine/Data.java index ce611589c..6d7b0c85b 100644 --- a/src/main/java/com/xceptance/xlt/api/engine/Data.java +++ b/src/main/java/com/xceptance/xlt/api/engine/Data.java @@ -150,4 +150,19 @@ public interface Data * the transaction's name */ public void setTransactionName(String transactionName); + + /** + * Returns the custom label determined by the labeling rules. Only used during report generation or analysis. + * + * @return the custom label + */ + public String getLabel(); + + /** + * Sets the custom label determined by the labeling rules. Only used during report generation or analysis. + * + * @param label + * the custom label + */ + public void setLabel(String label); } diff --git a/src/main/java/com/xceptance/xlt/report/DataParserThread.java b/src/main/java/com/xceptance/xlt/report/DataParserThread.java index babbdeb6f..e4329c141 100644 --- a/src/main/java/com/xceptance/xlt/report/DataParserThread.java +++ b/src/main/java/com/xceptance/xlt/report/DataParserThread.java @@ -33,6 +33,7 @@ import com.xceptance.xlt.api.report.PostProcessedDataContainer; import com.xceptance.xlt.api.util.SimpleArrayList; import com.xceptance.xlt.api.util.XltCharBuffer; +import com.xceptance.xlt.report.labelrules.LabelingRule; import com.xceptance.xlt.report.mergerules.RequestProcessingRule; import com.xceptance.xlt.report.mergerules.RequestProcessingRule.ReturnState; import com.zaxxer.sparsebits.SparseBitSet; @@ -117,6 +118,8 @@ public void run() final List requestProcessingRules = config.getRequestProcessingRules(); final boolean removeIndexes = config.getRemoveIndexesFromRequestNames(); + final List labelingRules = config.getLabelingRules(); + final double SAMPLELIMIT = 1 / ((double) config.dataSampleFactor); final int SAMPLEFACTOR = config.dataSampleFactor; @@ -218,7 +221,8 @@ public void run() } catch (final Exception ex) { - final String msg = String.format("Failed to parse data record at line %,d in file '%s': %s\nLine is: ", lineNumber, file, ex, lines.get(i).toString()); + final String msg = String.format("Failed to parse data record at line %,d in file '%s': %s\nLine is: ", lineNumber, + file, ex, lines.get(i).toString()); LOG.error(msg, ex); continue; @@ -230,17 +234,22 @@ public void run() // if this is request, filter it aka apply merge rules if (data instanceof RequestData) { - final RequestData result = postprocess((RequestData) data, requestProcessingRules, removeIndexes); - if (result != null) - { - postProcessedData.add(result); - } + // might return null in case of dropOnMatch + data = postprocess((RequestData) data, requestProcessingRules, removeIndexes); } else { // get us a hashcode for later while the cache is warm // for RequestData, we did that already data.getName().hashCode(); + } + + // check if merge rules did not drop our data item + if (data != null) + { + // set label + setLabel(data, labelingRules); + postProcessedData.add(data); } @@ -287,7 +296,6 @@ else if (adjustTimerName && (data instanceof RequestData || data instanceof Page } return data; - } /** @@ -295,16 +303,14 @@ else if (adjustTimerName && (data instanceof RequestData || data instanceof Page * discarding requests. * * @param requestData - * the request data record + * the request data record * @param requestProcessingRules - * the rules to apply + * the rules to apply * @param removeIndexesFromRequestNames - * in case we want to clean the name too - * + * in case we want to clean the name too * @return the processed request data record, or null if the data record is to be discarded */ - private RequestData postprocess(final RequestData requestData, - final List requestProcessingRules, + private RequestData postprocess(final RequestData requestData, final List requestProcessingRules, final boolean removeIndexesFromRequestNames) { // fix up the name first (Product.1.2 -> Product) if so configured @@ -356,12 +362,52 @@ else if (state == ReturnState.STOP) } } - // ok, we processed all rules for this dataset, get us the final hashcode for the name, because we need that later + // ok, we processed all rules for this dataset, get us the final hashcode for the name, because we need that + // later // here the cache is likely still hot, so this is less expensive requestData.getName().hashCode(); return requestData; } + /** + * Processes a data record according to the configured labeling rules. + * + * @param data + * the data record + * @param labelingRules + * the rules to apply + * @return the processed data record + */ + private void setLabel(final Data data, final List labelingRules) + { + // remember the original label so we can restore it in case labeling fails + final String originalLabel = data.getLabel(); + + // execute all labeling rules one after the other until processing is complete + final int size = labelingRules.size(); + for (int i = 0; i < size; i++) + { + final LabelingRule labelingRule = labelingRules.get(i); + + try + { + final com.xceptance.xlt.report.labelrules.LabelingRule.ReturnState state = labelingRule.process(data); + if (state == com.xceptance.xlt.report.labelrules.LabelingRule.ReturnState.STOP) + { + return; + } + } + catch (final Throwable t) + { + final String msg = String.format("Failed to apply labeling rule: %s\n%s", labelingRule, t); + LOG.error(msg); + // restore the data object's original label + data.setLabel(originalLabel); + + break; + } + } + } } diff --git a/src/main/java/com/xceptance/xlt/report/ReportGeneratorConfiguration.java b/src/main/java/com/xceptance/xlt/report/ReportGeneratorConfiguration.java index 9201f5994..2797aae9b 100644 --- a/src/main/java/com/xceptance/xlt/report/ReportGeneratorConfiguration.java +++ b/src/main/java/com/xceptance/xlt/report/ReportGeneratorConfiguration.java @@ -46,10 +46,12 @@ import com.xceptance.xlt.engine.XltExecutionContext; import com.xceptance.xlt.report.ReportGeneratorConfiguration.ChartCappingInfo.ChartCappingMethod; import com.xceptance.xlt.report.ReportGeneratorConfiguration.ChartCappingInfo.ChartCappingMode; +import com.xceptance.xlt.report.labelrules.LabelingRule; import com.xceptance.xlt.report.mergerules.InvalidRequestProcessingRuleException; import com.xceptance.xlt.report.mergerules.RequestProcessingRule; import com.xceptance.xlt.report.providers.RequestTableColorization; import com.xceptance.xlt.report.providers.RequestTableColorization.ColorizationRule; +import com.xceptance.xlt.util.rules.InvalidDataProcessingRuleException; /** * The ReportGeneratorConfiguration is the central place where all configuration information for the report generator @@ -79,14 +81,14 @@ public static class ChartCappingInfo */ public enum ChartCappingMethod { - /** No capping (default). */ - NONE, + /** No capping (default). */ + NONE, - /** Cap at an absolute value. */ - ABSOLUTE, + /** Cap at an absolute value. */ + ABSOLUTE, - /** Cap at the n-fold of the average value. */ - NFOLD_OF_AVERAGE + /** Cap at the n-fold of the average value. */ + NFOLD_OF_AVERAGE }; /** @@ -94,11 +96,11 @@ public enum ChartCappingMethod */ public enum ChartCappingMode { - /** Cap the chart at the capping value only if necessary. */ - SMART, + /** Cap the chart at the capping value only if necessary. */ + SMART, - /** Always cap the chart at the capping value even if the maximum values are below the capping value. */ - ALWAYS + /** Always cap the chart at the capping value even if the maximum values are below the capping value. */ + ALWAYS }; /** @@ -161,11 +163,17 @@ public enum ChartCappingMode private static final String PROP_REQUEST_MERGE_RULES_PREFIX = PROP_PREFIX + "requestMergeRules."; + private static final String PROP_LABELING_RULES_PREFIX = PROP_PREFIX + "labelingRules."; + // Special settings for profiling and debugging private static final String PROP_PARSER_THREAD_COUNT = PROP_PREFIX + "parser.threads"; + private static final String PROP_READER_THREAD_COUNT = PROP_PREFIX + "reader.threads"; + private static final String PROP_THREAD_QUEUE_SIZE = PROP_PREFIX + "queue.bucketsize"; + private static final String PROP_THREAD_QUEUE_LENGTH = PROP_PREFIX + "queue.length"; + private static final String PROP_DATA_SAMPLE_FACTOR = PROP_PREFIX + "data.sampleFactor"; private static final String PROP_TRANSFORMATIONS_PREFIX = PROP_PREFIX + "transformations."; @@ -243,8 +251,11 @@ public enum ChartCappingMode private boolean noAgentCharts; public final int readerThreadCount; + public final int parserThreadCount; + public final int threadQueueBucketSize; + public final int threadQueueLength; public final int dataSampleFactor; @@ -276,11 +287,11 @@ public enum ChartCappingMode private final int transactionErrorOverviewChartLimit; private final int errorDetailsChartLimit; - + private final int directoryLimitPerError; - + private final double directoryReplacementChance; - + private final int stackTracesLimit; private final Map apdexThresholdsByActionNamePattern = new HashMap<>(); @@ -408,9 +419,9 @@ public ReportGeneratorConfiguration(Properties xltProperties, final File overrid directoryLimitPerError = getIntProperty(XltPropertyNames.ReportGenerator.Errors.DIRECTORY_LIMIT_PER_ERROR, 10); directoryReplacementChance = getDoubleProperty(XltPropertyNames.ReportGenerator.Errors.DIRECTORY_REPLACEMENT_CHANCE, 0.1); - + stackTracesLimit = getIntProperty(XltPropertyNames.ReportGenerator.Errors.STACKTRACES_LIMIT, 500); - + // event settings groupEventsByTestCase = getBooleanProperty(PROP_PREFIX + "events.groupByTestCase", true); eventLimit = getIntProperty(PROP_PREFIX + "events.eventLimit", 100); @@ -791,7 +802,7 @@ public int getErrorDetailsChartLimit() { return errorDetailsChartLimit; } - + /** * The maximum number of directory hints remembered for a certain error (stack trace). * @@ -801,9 +812,10 @@ public int getDirectoryLimitPerError() { return directoryLimitPerError; } - + /** - * The chance to replace directory hints remembered for a certain error (stack trace) when the maximum number is reached. + * The chance to replace directory hints remembered for a certain error (stack trace) when the maximum number is + * reached. * * @return the chance to replace listed directory hints */ @@ -811,7 +823,7 @@ public double getDirectoryReplacementChance() { return directoryReplacementChance; } - + /** * The maximum number of errors that will be saved complete with their stack trace. * @@ -1001,7 +1013,6 @@ public int getEventLimitPerTestCase() return eventLimit; } - /** * Indicates whether or not to group events by test case. * @@ -1012,7 +1023,6 @@ public int getEventMessageLimitPerEvent() return eventMessageLimit; } - /** * Returns whether to automatically remove any indexes from the request name (i.e. "HomePage.1.27" -> "HomePage"). * @@ -1533,6 +1543,74 @@ public List getRequestProcessingRules() return requestProcessingRules; } + /** + * Reads and returns the configured labeling rules. + * + * @return the list of labeling rules + */ + public List getLabelingRules() + { + final List labelingRules = new ArrayList<>(); + + final Set ruleNumbers = new TreeSet<>(); + final Set ruleNumberStrings = getPropertyKeyFragment(PROP_LABELING_RULES_PREFIX); + for (final String s : ruleNumberStrings) + { + checkForLeadingZeros(s); + ruleNumbers.add(Integer.parseInt(s)); + } + + boolean invalidRulePresent = false; + for (final int i : ruleNumbers) + { + final String basePropertyName = PROP_LABELING_RULES_PREFIX + i; + + // general stuff + final String newlabel = getStringProperty(basePropertyName + ".newLabel", ""); + final boolean stopOnMatch = getBooleanProperty(basePropertyName + ".stopOnMatch", true); + + // include patterns + final String namePattern = getStringProperty(basePropertyName + ".namePattern", ""); + final String labelPattern = getStringProperty(basePropertyName + ".labelPattern", ""); + final String typeCodePattern = getStringProperty(basePropertyName + ".typeCodePattern", ""); + + // exclude patterns + final String nameExcludePattern = getStringProperty(basePropertyName + ".namePattern.exclude", ""); + final String labelExcludePattern = getStringProperty(basePropertyName + ".labelPattern.exclude", ""); + final String typeCodeExcludePattern = getStringProperty(basePropertyName + ".typeCodePattern.exclude", ""); + + // ensure that newLabel is set + if (StringUtils.isBlank(newlabel)) + { + throw new RuntimeException(String.format("Property '%s' is missing", basePropertyName + ".newLabel")); + } + + // create and validate the rules + try + { + final LabelingRule rule = new LabelingRule(newlabel, namePattern, labelPattern, typeCodePattern, stopOnMatch, + nameExcludePattern, labelExcludePattern, typeCodeExcludePattern); + labelingRules.add(rule); + } + catch (final InvalidDataProcessingRuleException e) + { + // Log it and continue with next rule. + final String errMsg = "Labeling rule '" + basePropertyName + "' is invalid. " + e.getMessage(); + XltLogger.reportLogger.error(errMsg, e); + System.err.println(errMsg); + // remember that we encountered an invalid merge rule + invalidRulePresent = true; + } + } + + if (invalidRulePresent) + { + throw new RuntimeException("Please check your configuration. At least one labeling rule is invalid and needs to be fixed."); + } + + return labelingRules; + } + /** * Reads the Apdex thresholds (per action group) from the configurations. */ diff --git a/src/main/java/com/xceptance/xlt/report/labelrules/LabelFilter.java b/src/main/java/com/xceptance/xlt/report/labelrules/LabelFilter.java new file mode 100644 index 000000000..fc39cae1b --- /dev/null +++ b/src/main/java/com/xceptance/xlt/report/labelrules/LabelFilter.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2005-2024 Xceptance Software Technologies GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.xceptance.xlt.report.labelrules; + +import org.apache.commons.lang3.StringUtils; + +import com.xceptance.xlt.api.engine.Data; +import com.xceptance.xlt.util.rules.AbstractPatternFilter; + +/** + * Filters data records based on their label. + */ +public class LabelFilter extends AbstractPatternFilter +{ + /** + * Constructor. + * + * @param regex + * the regular expression to identify matching data records + */ + public LabelFilter(final String regex) + { + this(regex, false); + } + + /** + * Constructor. + * + * @param regex + * the regular expression to identify matching data records + * @param exclude + * whether or not this is an exclusion rule + */ + public LabelFilter(final String regex, final boolean exclude) + { + super("l", regex, exclude, 600); + } + + /** + * {@inheritDoc} + */ + @Override + protected CharSequence getText(final Data data) + { + String label = data.getLabel(); + + return label == null ? StringUtils.EMPTY : label; + } +} diff --git a/src/main/java/com/xceptance/xlt/report/labelrules/LabelingRule.java b/src/main/java/com/xceptance/xlt/report/labelrules/LabelingRule.java new file mode 100644 index 000000000..9db9fd9af --- /dev/null +++ b/src/main/java/com/xceptance/xlt/report/labelrules/LabelingRule.java @@ -0,0 +1,242 @@ +/* + * Copyright (c) 2005-2024 Xceptance Software Technologies GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.xceptance.xlt.report.labelrules; + +import java.util.ArrayList; +import java.util.regex.PatternSyntaxException; + +import org.apache.commons.lang3.StringUtils; + +import com.xceptance.xlt.api.engine.Data; +import com.xceptance.xlt.util.rules.AbstractDataProcessingRule; +import com.xceptance.xlt.util.rules.AbstractFilter; +import com.xceptance.xlt.util.rules.InvalidDataProcessingRuleException; +import com.xceptance.xlt.util.rules.PlaceholderPosition; + +/** + * A {@link LabelingRule} governs the process of labeling data objects. It represents a bundle of criteria a data object + * must meet to be labeled and defines how the new label will look like. + * + * @see Data#setLabel(String) + */ +public class LabelingRule extends AbstractDataProcessingRule +{ + /** + * Our return states to ensure correct communication of the result + */ + public static enum ReturnState + { + STOP, + CONTINUE + }; + + /** + * The definition of the new label including placeholders. + */ + private final String newLabel; + + /** + * Whether or not to process the next rule if the current rule applied. + */ + private final boolean stopOnMatch; + + /** + * The list of configured request filters of this rule. + */ + private final AbstractFilter[] filters; + + /** + * The list of placeholders (with their position, etc.) in the new name. + */ + private final PlaceholderPosition[] newLabelPlaceholders; + + /** + * Constructor. + * + * @param newLabel + * @param namePattern + * @param labelPattern + * @param typeCodePattern + * @param stopOnMatch + * @param nameExcludePattern + * @param labelExcludePattern + * @param typeCodeExcludePattern + * @throws InvalidLabelingRuleException + */ + public LabelingRule(final String newLabel, final String namePattern, final String labelPattern, String typeCodePattern, + final boolean stopOnMatch, final String nameExcludePattern, final String labelExcludePattern, + String typeCodeExcludePattern) + throws InvalidDataProcessingRuleException + { + this.newLabel = newLabel; + this.stopOnMatch = stopOnMatch; + + final ArrayList> requestFilters = new ArrayList<>(20); + + // parse the placeholder positions now (and only once) + newLabelPlaceholders = parsePlaceholderPositions(newLabel); + try + { // includes and source of data if not empty and configured + addIfTypeCodeInNewName(requestFilters, new NameFilter(namePattern), namePattern, newLabelPlaceholders); + addIfTypeCodeInNewName(requestFilters, new LabelFilter(labelPattern), labelPattern, newLabelPlaceholders); + addIfTypeCodeInNewName(requestFilters, new TypeCodeFilter(typeCodePattern), typeCodePattern, newLabelPlaceholders); + + // excludes + if (StringUtils.isNotBlank(nameExcludePattern)) + { + requestFilters.add(new NameFilter(nameExcludePattern, true)); + } + + if (StringUtils.isNotBlank(labelExcludePattern)) + { + requestFilters.add(new LabelFilter(labelExcludePattern, true)); + } + + if (StringUtils.isNotBlank(typeCodeExcludePattern)) + { + requestFilters.add(new TypeCodeFilter(typeCodeExcludePattern, true)); + } + } + catch (final PatternSyntaxException pse) + { + throw new InvalidDataProcessingRuleException("Invalid regular expression: " + pse.getPattern()); + } + + this.filters = requestFilters.toArray(new AbstractFilter[requestFilters.size()]); + + // Validate the entire rule. + validateRule(filters, newLabelPlaceholders); + } + + /** + * Processes this request merge rule. As a consequence, the passed request data object will (or will not) get a new + * name. This is a multi-threaded routine aka in use by several threads at the same time. + * + * @param data + * the data object to process, will also be directly modified as result + * @return true if we want to stop, false otherwise + */ + public ReturnState process(final Data data) + { + // try each filter and remember its state for later processing + final int requestFiltersSize = filters.length; + // we can allocate that here because it is small and will live on the stack, + // hence will not create GC pressure and will be hit in the cache + final Object[] filterStates = new Object[requestFiltersSize]; + + for (int i = 0; i < requestFiltersSize; i++) + { + final AbstractFilter filter = filters[i]; + final var state = filter.appliesTo(data); + + if (state == null) + { + // return early since one of the filters did *not* apply + + // continue request processing with an unmodified result + return ReturnState.CONTINUE; + } + filterStates[i] = state; + } + + // all filters applied so we can process the request, but check first what to do + // anything to do? + if (newLabelPlaceholders.length > 0) + { + // rename the request + final StringBuilder result = new StringBuilder(newLabel); + + // search as long as there are placeholders in the name + int displacement = 0; + for (final PlaceholderPosition placeholder : newLabelPlaceholders) + { + // find the corresponding filter and filter state + for (int i = 0; i < requestFiltersSize; i++) + { + final AbstractFilter requestFilter = filters[i]; + + // check if this is our type code (compare it efficiently) + if (requestFilter.isSameTypeCode(placeholder.typeCode, placeholder.typeCodeHashCode)) + { + final int capturingGroupIndex = placeholder.index; + final Object filterState = filterStates[i]; + + // get replacement + final CharSequence replacement = requestFilter.getReplacementText(data, capturingGroupIndex, filterState); + + // replace the placeholder with the real values + result.delete(placeholder.start + displacement, placeholder.end + displacement); + result.insert(placeholder.start + displacement, replacement); + + // adjust the displacement for the next replace + displacement += replacement.length() - placeholder.length; + + break; + } + } + } + + // set the final name + data.setLabel(result.toString()); + } + else + { + // nothing to do, keep the newName + data.setLabel(newLabel); + } + + return stopOnMatch ? ReturnState.STOP : ReturnState.CONTINUE; + } + + /** + * {@inheritDoc} + */ + @Override + public String toString() + { + final StringBuilder sb = new StringBuilder(); + sb.append("Labeling rule: '").append(newLabel).append("', filters: ["); + boolean appendComma = false; + for (final AbstractFilter filter : filters) + { + final String typeCode = filter.getTypeCode(); + + if (appendComma) + { + sb.append(", "); + } + + // nl + if ("n".equals(typeCode)) + { + sb.append("name: ").append(filter.toString()); + } + else if ("l".equals(typeCode)) + { + sb.append("label: ").append(filter.toString()); + } + else + { + sb.append("unknown"); + } + + appendComma = true; + } + sb.append("]"); + + return sb.toString(); + } +} diff --git a/src/main/java/com/xceptance/xlt/report/labelrules/NameFilter.java b/src/main/java/com/xceptance/xlt/report/labelrules/NameFilter.java new file mode 100644 index 000000000..93fdec2b9 --- /dev/null +++ b/src/main/java/com/xceptance/xlt/report/labelrules/NameFilter.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2005-2024 Xceptance Software Technologies GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.xceptance.xlt.report.labelrules; + +import com.xceptance.xlt.api.engine.Data; +import com.xceptance.xlt.util.rules.AbstractPatternFilter; + +/** + * Filters data records based on their name. + */ +public class NameFilter extends AbstractPatternFilter +{ + /** + * Constructor. + * + * @param regex + * the regular expression to identify matching data records + */ + public NameFilter(final String regex) + { + this(regex, false); + } + + /** + * Constructor. + * + * @param regex + * the regular expression to identify matching data records + * @param exclude + * whether or not this is an exclusion rule + */ + public NameFilter(final String regex, final boolean exclude) + { + super("n", regex, exclude, 600); + } + + /** + * {@inheritDoc} + */ + @Override + protected CharSequence getText(final Data data) + { + return data.getName(); + } +} diff --git a/src/main/java/com/xceptance/xlt/report/labelrules/TypeCodeFilter.java b/src/main/java/com/xceptance/xlt/report/labelrules/TypeCodeFilter.java new file mode 100644 index 000000000..5f54ef973 --- /dev/null +++ b/src/main/java/com/xceptance/xlt/report/labelrules/TypeCodeFilter.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2005-2024 Xceptance Software Technologies GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.xceptance.xlt.report.labelrules; + +import com.xceptance.xlt.api.engine.Data; +import com.xceptance.xlt.util.rules.AbstractPatternFilter; + +/** + * Filters data records based on their type code. + */ +public class TypeCodeFilter extends AbstractPatternFilter +{ + /** + * Constructor. + * + * @param regex + * the regular expression to identify matching data records + */ + public TypeCodeFilter(final String regex) + { + this(regex, false); + } + + /** + * Constructor. + * + * @param regex + * the regular expression to identify matching data records + * @param exclude + * whether or not this is an exclusion rule + */ + public TypeCodeFilter(final String regex, final boolean exclude) + { + super("t", regex, exclude, 600); + } + + /** + * {@inheritDoc} + */ + @Override + protected CharSequence getText(final Data data) + { + return String.valueOf(data.getTypeCode()); + } +} diff --git a/src/main/java/com/xceptance/xlt/report/providers/BasicTimerDataProcessor.java b/src/main/java/com/xceptance/xlt/report/providers/BasicTimerDataProcessor.java index 2deff1787..5d5072f8c 100644 --- a/src/main/java/com/xceptance/xlt/report/providers/BasicTimerDataProcessor.java +++ b/src/main/java/com/xceptance/xlt/report/providers/BasicTimerDataProcessor.java @@ -15,6 +15,8 @@ */ package com.xceptance.xlt.report.providers; +import java.util.Set; + import org.jfree.chart.JFreeChart; import org.jfree.chart.plot.CombinedDomainXYPlot; import org.jfree.chart.plot.CombinedRangeXYPlot; @@ -30,11 +32,11 @@ import com.xceptance.xlt.report.ReportGeneratorConfiguration; import com.xceptance.xlt.report.ReportGeneratorConfiguration.ChartScale; import com.xceptance.xlt.report.util.FixedSizeHistogramValueSet; -import com.xceptance.xlt.report.util.JFreeChartUtils; import com.xceptance.xlt.report.util.IntMinMaxValueSet; +import com.xceptance.xlt.report.util.IntSummaryStatistics; +import com.xceptance.xlt.report.util.JFreeChartUtils; import com.xceptance.xlt.report.util.ReportUtils; import com.xceptance.xlt.report.util.RuntimeHistogram; -import com.xceptance.xlt.report.util.IntSummaryStatistics; import com.xceptance.xlt.report.util.TaskManager; import com.xceptance.xlt.report.util.ValueSet; @@ -66,6 +68,8 @@ public class BasicTimerDataProcessor extends AbstractDataProcessor private final int minMaxValueSetSize; + private String label; + /** * Constructor. * @@ -108,6 +112,7 @@ public TimerReport createTimerReport(final boolean generateHistogram) timerReport.max = runTimeStatistics.getMaximum(); timerReport.min = runTimeStatistics.getMinimum(); timerReport.name = name; + timerReport.labels = (label == null) ? null : Set.of(label); timerReport.deviation = ReportUtils.convertToBigDecimal(runTimeStatistics.getStandardDeviation()); timerReport.median = ReportUtils.convertToBigDecimal(runTimeHistogram.getMedianValue()); @@ -121,7 +126,7 @@ public TimerReport createTimerReport(final boolean generateHistogram) // set the counts final double count = runTimeStatistics.getCount(); final long duration = Math.max((getEndTime() - getStartTime()) / 1000, 1); - + timerReport.errorPercentage = ReportUtils.calculatePercentage(totalErrors, (int) count); timerReport.count = (int) count; timerReport.countPerSecond = ReportUtils.convertToBigDecimal(count / duration); @@ -213,6 +218,9 @@ public void processDataRecord(final Data stat) // we expect the timer to be failed around the same time as it has finished errorsPerSecondValueSet.addOrUpdateValue(endTime, 1); } + + // remember the latest label (it should always be the same) + this.label = stat.getLabel(); } protected ValueSet getCountPerSecondValueSet() @@ -250,8 +258,8 @@ protected FixedSizeHistogramValueSet getHistogramValueSet() */ protected JFreeChart createResponseTimeAndErrorsChart(final String chartTitle, final TimeSeries responseTimeSeries, final TimeSeries responseTimeAverageSeries, - final XYIntervalSeries responseTimeHistogramSeries, - final TimeSeries errorsSeries, final int chartCappingValue) + final XYIntervalSeries responseTimeHistogramSeries, final TimeSeries errorsSeries, + final int chartCappingValue) { final ChartScale chartScale = ((ReportGeneratorConfiguration) getConfiguration()).getChartScale(); Range responseTimeRange = null; diff --git a/src/main/java/com/xceptance/xlt/report/providers/TimerReport.java b/src/main/java/com/xceptance/xlt/report/providers/TimerReport.java index b084cffee..b91fe86fa 100644 --- a/src/main/java/com/xceptance/xlt/report/providers/TimerReport.java +++ b/src/main/java/com/xceptance/xlt/report/providers/TimerReport.java @@ -18,10 +18,12 @@ import java.math.BigDecimal; import java.util.LinkedHashMap; import java.util.Map; +import java.util.Set; import com.thoughtworks.xstream.annotations.XStreamAlias; import com.thoughtworks.xstream.annotations.XStreamConverter; import com.xceptance.xlt.report.util.CustomMapConverter; +import com.xceptance.xlt.report.util.LabelSetConverter; /** * Represents the timer statistics in a test report. The statistics is generated from a series of timer events. @@ -34,6 +36,12 @@ public class TimerReport */ public String name; + /** + * The labels. + */ + @XStreamConverter(LabelSetConverter.class) + public Set labels; + /** * The number how often the timer has fired. */ diff --git a/src/main/java/com/xceptance/xlt/report/util/LabelSetConverter.java b/src/main/java/com/xceptance/xlt/report/util/LabelSetConverter.java new file mode 100644 index 000000000..025f1f72a --- /dev/null +++ b/src/main/java/com/xceptance/xlt/report/util/LabelSetConverter.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2005-2024 Xceptance Software Technologies GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.xceptance.xlt.report.util; + +/** + * A custom XStream converter that converts a set of labels. The resulting output is as follows: + * + *
+ *     <label>foo</label>
+ *     <label>bar</label>
+ * 
+ * + * Note that this converter supports streaming only, but not parsing. + */ +public class LabelSetConverter extends NamedIterableConverter +{ + public LabelSetConverter() + { + super("label"); + } +} diff --git a/src/main/java/com/xceptance/xlt/report/util/NamedIterableConverter.java b/src/main/java/com/xceptance/xlt/report/util/NamedIterableConverter.java new file mode 100644 index 000000000..60127ea21 --- /dev/null +++ b/src/main/java/com/xceptance/xlt/report/util/NamedIterableConverter.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2005-2024 Xceptance Software Technologies GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.xceptance.xlt.report.util; + +import com.thoughtworks.xstream.converters.Converter; +import com.thoughtworks.xstream.converters.MarshallingContext; +import com.thoughtworks.xstream.converters.UnmarshallingContext; +import com.thoughtworks.xstream.io.HierarchicalStreamReader; +import com.thoughtworks.xstream.io.HierarchicalStreamWriter; + +/** + * A custom XStream converter that converts any class implementing {@link Iterable} in such a way that its values are + * streamed as <tag>value</tag> where the name of the tag can be customized. + *

+ * Note that this converter supports streaming only, but not parsing. + */ +public class NamedIterableConverter implements Converter +{ + private final String tagName; + + protected NamedIterableConverter(final String tagName) + { + this.tagName = tagName; + } + + @Override + public boolean canConvert(@SuppressWarnings("rawtypes") final Class type) + { + return Iterable.class.isAssignableFrom(type); + } + + @Override + public void marshal(final Object source, final HierarchicalStreamWriter writer, final MarshallingContext context) + { + final Iterable iterable = (Iterable) source; + for (final Object value : iterable) + { + writer.startNode(tagName); + writer.setValue(value.toString()); + writer.endNode(); + } + } + + @Override + public Object unmarshal(final HierarchicalStreamReader reader, final UnmarshallingContext context) + { + throw new UnsupportedOperationException(); + } +} diff --git a/src/main/java/com/xceptance/xlt/util/rules/AbstractDataProcessingRule.java b/src/main/java/com/xceptance/xlt/util/rules/AbstractDataProcessingRule.java new file mode 100644 index 000000000..2f8e42093 --- /dev/null +++ b/src/main/java/com/xceptance/xlt/util/rules/AbstractDataProcessingRule.java @@ -0,0 +1,156 @@ +/* + * Copyright (c) 2005-2024 Xceptance Software Technologies GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.xceptance.xlt.util.rules; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import com.xceptance.common.util.RegExUtils; +import com.xceptance.xlt.api.engine.Data; + +/** + * Base class for data processing rules. At the moment, this class provides common helpers only. + */ +public class AbstractDataProcessingRule +{ + /** + * The pattern to find placeholders in the new label. + */ + private static final Pattern PLACEHOLDER_PATTERN = Pattern.compile("\\{([nl])(?::([0-9]+))?\\}"); + + /** + * Adds this filter to the filter rule list if the pattern is not empty and the results is later needed in the new + * name, otherwise we just ignore it to save cyles + */ + protected void addIfTypeCodeInNewName(final List> filters, final AbstractFilter filter, final String pattern, + final PlaceholderPosition[] newNamePlaceholders) + { + final String typeCode = filter.getTypeCode(); + + // if the pattern is empty, we need to know if we might need the data anyway + if (pattern == null || "".equals(pattern)) + { + // add the filter only if we need it as source of data + for (final PlaceholderPosition p : newNamePlaceholders) + { + if (p.typeCode.equals(typeCode)) + { + // yes, we play a role + filters.add(filter); + return; + } + } + } + else + { + // the pattern is not empty, add it + filters.add(filter); + } + + // well, we don't add it, because we don't need it + } + + /** + * Parses the position of the placeholders in the new name field of the rule. + * + * @param newNameWithPlaceholders + * the new name containing placeholders + * @return the placeholder positions found + * @throws InvalidLabelingRuleException + */ + protected PlaceholderPosition[] parsePlaceholderPositions(final String newNameWithPlaceholders) + throws InvalidDataProcessingRuleException + { + final Matcher matcher = PLACEHOLDER_PATTERN.matcher(newNameWithPlaceholders); + + // use a list now and make it an array later + final List positions = new ArrayList<>(); + + while (matcher.find()) + { + // determine the matching group index if any was present at all + int matchingGroupIndex = -1; + + final String matchingGroupIndexString = matcher.group(2); + if (matchingGroupIndexString != null) + { + try + { + matchingGroupIndex = Integer.valueOf(matchingGroupIndexString); + } + catch (final NumberFormatException e) + { + throw new InvalidDataProcessingRuleException("Failed to parse the matching group index '" + matchingGroupIndexString + + "' as integer"); + } + } + + // build and remember the placeholder position + final PlaceholderPosition position = new PlaceholderPosition(matcher.group(1), matchingGroupIndex, matcher.start(), + matcher.end(), matcher.group().length()); + positions.add(position); + } + + // get us an efficient array for later, we won't change things anymore + return positions.toArray(new PlaceholderPosition[positions.size()]); + } + + protected void validateRule(final AbstractFilter[] filters, final PlaceholderPosition[] newPlaceholderPositions) + throws InvalidDataProcessingRuleException + { + for (final PlaceholderPosition placeholderPosition : newPlaceholderPositions) + { + for (final AbstractFilter filter : filters) + { + if (filter.getTypeCode().equals(placeholderPosition.typeCode)) + { + if (filter instanceof AbstractPatternFilter) + { + final AbstractPatternFilter patternFilter = (AbstractPatternFilter) filter; + + // TODO: #3252 + // if (placeholderPosition.index != -1) + // { + // // check that we have a pattern at all + // if (patternFilter.isEmpty()) + // { + // throw new InvalidRequestProcessingRuleException(String.format("Matching group '%d' specified, + // but there is no pattern", + // placeholderPosition.index)); + // } + // } + + if (placeholderPosition.index > 0) + { + // check that the filter pattern has the wanted matching group + final String pattern = patternFilter.getPattern(); + final int nbCaptureGroups = RegExUtils.getCaptureGroupCount(pattern); + if (placeholderPosition.index > nbCaptureGroups) + { + throw new InvalidDataProcessingRuleException(String.format("Pattern '%s' has no matching group '%d'", + pattern, placeholderPosition.index)); + } + } + } + + break; + } + } + } + } +} diff --git a/src/main/java/com/xceptance/xlt/util/rules/AbstractFilter.java b/src/main/java/com/xceptance/xlt/util/rules/AbstractFilter.java new file mode 100644 index 000000000..528fefa83 --- /dev/null +++ b/src/main/java/com/xceptance/xlt/util/rules/AbstractFilter.java @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2005-2024 Xceptance Software Technologies GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.xceptance.xlt.util.rules; + +import com.xceptance.xlt.api.engine.Data; +import com.xceptance.xlt.api.engine.RequestData; + +/** + * Base class for all data record filters. + */ +public abstract class AbstractFilter +{ + /** + * The type code of this request filter. + */ + private final String typeCode; + + /** + * The hash of the string for faster comparison of the type code + */ + private final int typeCodeHashCode; + + /** + * Constructor. + * + * @param typeCode + * the type code of this request filter + */ + public AbstractFilter(final String typeCode) + { + this.typeCode = typeCode; + typeCodeHashCode = typeCode.hashCode(); + } + + /** + * Returns the replacement text derived from the passed data object. + * + * @param data + * the data object + * @param capturingGroupIndex + * the capturing group index specified in the placeholder + * @param filterState + * the filter state object returned by {@link #appliesTo(RequestData)} + * @return the replacement text + */ + public abstract CharSequence getReplacementText(T data, int capturingGroupIndex, Object filterState); + + /** + * Whether or not the passed data object is accepted by this filter. + * + * @param data + * the data object + * @return in case the filter accepted the passed data object: a state object representing the filter state (can be + * a dummy object), otherwise: null + */ + public abstract Object appliesTo(T data); + + /** + * Returns the type code of this filter. + * + * @return the type code + */ + public String getTypeCode() + { + return typeCode; + } + + /** + * Compares two types codes efficiently + * + * @param typeCode + * the type code to compare to + * @param typeCodeHashCode + * the hash of the type code for performance reason + * @return true, if the type codes match, false otherwise + */ + public boolean isSameTypeCode(final String typeCode, final int typeCodeHashCode) + { + if (this.typeCodeHashCode == typeCodeHashCode) + { + return this.typeCode.equals(typeCode); + } + else + { + return false; + } + } +} diff --git a/src/main/java/com/xceptance/xlt/util/rules/AbstractPatternFilter.java b/src/main/java/com/xceptance/xlt/util/rules/AbstractPatternFilter.java new file mode 100644 index 000000000..857f51497 --- /dev/null +++ b/src/main/java/com/xceptance/xlt/util/rules/AbstractPatternFilter.java @@ -0,0 +1,212 @@ +/* + * Copyright (c) 2005-2024 Xceptance Software Technologies GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.xceptance.xlt.util.rules; + +import java.util.regex.MatchResult; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.apache.commons.lang3.StringUtils; + +import com.xceptance.common.collection.LRUFastHashMap; +import com.xceptance.common.lang.ThrowableUtils; +import com.xceptance.common.util.RegExUtils; +import com.xceptance.xlt.api.engine.Data; + +/** + * Base class for all data record filters that use regular expressions to identify matching data records. + */ +public abstract class AbstractPatternFilter extends AbstractFilter +{ + /** + * Cache the expensive stuff, we are a per thread instance. Can be empty! + */ + private final LRUFastHashMap cache; + + /** + * Just a place holder for a NULL + */ + private static final MatchResult NULL = Pattern.compile(".*").matcher("null").toMatchResult(); + + /** + * The matcher we use when we don't want to cache anything + */ + private final Matcher matcher; + + /** + * Whether or not this is an exclusion rule. + */ + private final boolean isExclude; + + /** + * Constructor. + * + * @param typeCode + * the type code of this filter + * @param regex + * the regular expression to identify matching data objects + */ + public AbstractPatternFilter(final String typeCode, final String regex) + { + // default cache + this(typeCode, regex, false, 100); + } + + /** + * Constructor. + * + * @param typeCode + * the type code of this filter + * @param regex + * the regular expression to identify matching data objects + * @param exclude + * whether or not this is an exclusion rule + */ + public AbstractPatternFilter(final String typeCode, final String regex, final boolean exclude, final int cacheSize) + { + super(typeCode); + + matcher = StringUtils.isBlank(regex) ? null : RegExUtils.getPattern(regex, 0).matcher("any"); + isExclude = exclude; + cache = cacheSize > 0 ? new LRUFastHashMap<>(cacheSize) : null; + } + + /** + * Returns the text to examine from the passed data object. + * + * @return the text + */ + protected abstract CharSequence getText(final T data); + + /** + * {@inheritDoc} + */ + @Override + public Object appliesTo(final T data) + { + if (matcher == null) + { + // empty is always fine, we just want to get the full text -> return a non-null dummy object + return Boolean.TRUE; + } + + // get the data to match against + final CharSequence text = getText(data); + + // only cache if we want that, there are areas where caching does not make sense and wastes + // a lot of time + if (cache == null) + { + // reuse our matcher and save memory + final Matcher m = matcher.reset(text); + + // when we return the matcher, it will be evaluated instantly and + // hence is reuseable during the next call, this saves memory + // because a matcher is large + return (m.find() ^ isExclude) ? m : null; + } + else + { + MatchResult result = cache.get(text); + if (result == null) + { + // not found, produce and cache, recycle the matcher + // cache only the result, not the matcher itself + final Matcher m = matcher.reset(text); + + if (m.find() ^ isExclude) + { + // we don't cache the matcher but the result which is immutable + result = m.toMatchResult(); + cache.put(text, result); + + return result; + } + else + { + // remember the miss + cache.put(text, NULL); + return null; + } + } + + // ok, we got one, just see if this is NULL or a match + return result == NULL ? null : result; + } + } + + /** + * {@inheritDoc} + */ + @Override + public CharSequence getReplacementText(final T data, final int capturingGroupIndex, final Object filterState) + { + if (isExclude || matcher == null || capturingGroupIndex == -1) + { + return getText(data); + } + + try + { + return ((MatchResult) filterState).group(capturingGroupIndex); + } + catch (final IndexOutOfBoundsException ioobe) + { + final String format = "No matching group %d for input string '%s' and pattern '%s'"; + ThrowableUtils.setMessage(ioobe, String.format(format, capturingGroupIndex, getText(data), getPattern())); + + throw ioobe; + } + } + + /** + * {@inheritDoc} + */ + @Override + public String toString() + { + final StringBuilder sb = new StringBuilder("{ type: '"); + sb.append(getTypeCode()).append("', "); + sb.append("pattern: '").append(getPattern()).append("', "); + sb.append("isExclude: ").append(isExclude).append(" }"); + + return sb.toString(); + } + + /** + * Returns the filter pattern string. + */ + public String getPattern() + { + return (matcher == null) ? StringUtils.EMPTY : matcher.pattern().pattern(); + } + + /** + * Whether this filter has an empty pattern. + */ + public boolean isEmpty() + { + return matcher == null; + } + + /** + * Whether this filter is an exclude filter. + */ + public boolean isExclude() + { + return isExclude; + } +} diff --git a/src/main/java/com/xceptance/xlt/util/rules/InvalidDataProcessingRuleException.java b/src/main/java/com/xceptance/xlt/util/rules/InvalidDataProcessingRuleException.java new file mode 100644 index 000000000..aced96b6b --- /dev/null +++ b/src/main/java/com/xceptance/xlt/util/rules/InvalidDataProcessingRuleException.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2005-2024 Xceptance Software Technologies GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.xceptance.xlt.util.rules; + +/** + * Exception used to indicate an invalid data processing rule. + */ +public class InvalidDataProcessingRuleException extends Exception +{ + public InvalidDataProcessingRuleException() + { + } + + public InvalidDataProcessingRuleException(final String message) + { + super(message); + } +} diff --git a/src/main/java/com/xceptance/xlt/util/rules/PlaceholderPosition.java b/src/main/java/com/xceptance/xlt/util/rules/PlaceholderPosition.java new file mode 100644 index 000000000..6e4eb2fc9 --- /dev/null +++ b/src/main/java/com/xceptance/xlt/util/rules/PlaceholderPosition.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2005-2024 Xceptance Software Technologies GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.xceptance.xlt.util.rules; + +/** + * Container for placeholder information within a pattern. It is thread safe and can be shared. + * + * @author rschwietzke + */ +public class PlaceholderPosition +{ + public final String typeCode; + + public final int typeCodeHashCode; + + public final int index; + + public final int start; + + public final int end; + + public final int length; + + public PlaceholderPosition(final String typeCode, final int index, final int start, final int end, final int length) + { + this.typeCode = typeCode; + typeCodeHashCode = typeCode.hashCode(); + + this.index = index; + this.start = start; + this.end = end; + this.length = length; + } +}