All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.flink.cep.pattern.Pattern Maven / Gradle / Ivy

There is a newer version: 2.0-preview1
Show newest version
/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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 org.apache.flink.cep.pattern;

import org.apache.flink.api.common.ExecutionConfig;
import org.apache.flink.api.java.ClosureCleaner;
import org.apache.flink.cep.nfa.NFA;
import org.apache.flink.cep.nfa.aftermatch.AfterMatchSkipStrategy;
import org.apache.flink.cep.pattern.Quantifier.ConsumingStrategy;
import org.apache.flink.cep.pattern.Quantifier.Times;
import org.apache.flink.cep.pattern.conditions.BooleanConditions;
import org.apache.flink.cep.pattern.conditions.IterativeCondition;
import org.apache.flink.cep.pattern.conditions.RichAndCondition;
import org.apache.flink.cep.pattern.conditions.RichOrCondition;
import org.apache.flink.cep.pattern.conditions.SubtypeCondition;
import org.apache.flink.streaming.api.windowing.time.Time;
import org.apache.flink.util.Preconditions;

import javax.annotation.Nullable;

import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

/**
 * Base class for a pattern definition.
 *
 * 

A pattern definition is used by {@link org.apache.flink.cep.nfa.compiler.NFACompiler} to * create a {@link NFA}. * *

{@code
 * Pattern pattern = Pattern.begin("start")
 *   .next("middle").subtype(F.class)
 *   .followedBy("end").where(new MyCondition());
 * }
* * @param Base type of the elements appearing in the pattern * @param Subtype of T to which the current pattern operator is constrained */ public class Pattern { /** Name of the pattern. */ private final String name; /** Previous pattern. */ private final Pattern previous; /** The condition an event has to satisfy to be considered a matched. */ private IterativeCondition condition; /** Window length in which the pattern match has to occur. */ private final Map windowTimes = new HashMap<>(); /** * A quantifier for the pattern. By default set to {@link Quantifier#one(ConsumingStrategy)}. */ private Quantifier quantifier = Quantifier.one(ConsumingStrategy.STRICT); /** The condition an event has to satisfy to stop collecting events into looping state. */ private IterativeCondition untilCondition; /** Applicable to a {@code times} pattern, and holds the number of times it has to appear. */ private Times times; private final AfterMatchSkipStrategy afterMatchSkipStrategy; protected Pattern( final String name, final Pattern previous, final ConsumingStrategy consumingStrategy, final AfterMatchSkipStrategy afterMatchSkipStrategy) { this.name = name; this.previous = previous; this.quantifier = Quantifier.one(consumingStrategy); this.afterMatchSkipStrategy = afterMatchSkipStrategy; } public Pattern getPrevious() { return previous; } public Times getTimes() { return times; } public String getName() { return name; } /** @deprecated Use {@link #getWindowSize()} */ @Deprecated @Nullable public Time getWindowTime() { return getWindowSize().map(Time::of).orElse(null); } public Optional getWindowSize() { return getWindowSize(WithinType.FIRST_AND_LAST); } /** @deprecated Use {@link #getWindowSize(WithinType)}. */ @Deprecated @Nullable public Time getWindowTime(WithinType withinType) { return getWindowSize(withinType).map(Time::of).orElse(null); } public Optional getWindowSize(WithinType withinType) { return Optional.ofNullable(windowTimes.get(withinType)); } public Quantifier getQuantifier() { return quantifier; } public IterativeCondition getCondition() { if (condition != null) { return condition; } else { return BooleanConditions.trueFunction(); } } public IterativeCondition getUntilCondition() { return untilCondition; } /** * Starts a new pattern sequence. The provided name is the one of the initial pattern of the new * sequence. Furthermore, the base type of the event sequence is set. * * @param name The name of starting pattern of the new pattern sequence * @param Base type of the event pattern * @return The first pattern of a pattern sequence */ public static Pattern begin(final String name) { return new Pattern<>(name, null, ConsumingStrategy.STRICT, AfterMatchSkipStrategy.noSkip()); } /** * Starts a new pattern sequence. The provided name is the one of the initial pattern of the new * sequence. Furthermore, the base type of the event sequence is set. * * @param name The name of starting pattern of the new pattern sequence * @param afterMatchSkipStrategy the {@link AfterMatchSkipStrategy.SkipStrategy} to use after * each match. * @param Base type of the event pattern * @return The first pattern of a pattern sequence */ public static Pattern begin( final String name, final AfterMatchSkipStrategy afterMatchSkipStrategy) { return new Pattern(name, null, ConsumingStrategy.STRICT, afterMatchSkipStrategy); } /** * Adds a condition that has to be satisfied by an event in order to be considered a match. If * another condition has already been set, the new one is going to be combined with the previous * with a logical {@code AND}. In other case, this is going to be the only condition. * * @param condition The condition as an {@link IterativeCondition}. * @return The pattern with the new condition is set. */ public Pattern where(IterativeCondition condition) { Preconditions.checkNotNull(condition, "The condition cannot be null."); ClosureCleaner.clean(condition, ExecutionConfig.ClosureCleanerLevel.RECURSIVE, true); if (this.condition == null) { this.condition = condition; } else { this.condition = new RichAndCondition<>(this.condition, condition); } return this; } /** * Adds a condition that has to be satisfied by an event in order to be considered a match. If * another condition has already been set, the new one is going to be combined with the previous * with a logical {@code OR}. In other case, this is going to be the only condition. * * @param condition The condition as an {@link IterativeCondition}. * @return The pattern with the new condition is set. */ public Pattern or(IterativeCondition condition) { Preconditions.checkNotNull(condition, "The condition cannot be null."); ClosureCleaner.clean(condition, ExecutionConfig.ClosureCleanerLevel.RECURSIVE, true); if (this.condition == null) { this.condition = condition; } else { this.condition = new RichOrCondition<>(this.condition, condition); } return this; } /** * Applies a subtype constraint on the current pattern. This means that an event has to be of * the given subtype in order to be matched. * * @param subtypeClass Class of the subtype * @param Type of the subtype * @return The same pattern with the new subtype constraint */ public Pattern subtype(final Class subtypeClass) { Preconditions.checkNotNull(subtypeClass, "The class cannot be null."); if (condition == null) { this.condition = new SubtypeCondition(subtypeClass); } else { this.condition = new RichAndCondition<>(condition, new SubtypeCondition(subtypeClass)); } @SuppressWarnings("unchecked") Pattern result = (Pattern) this; return result; } /** * Applies a stop condition for a looping state. It allows cleaning the underlying state. * * @param untilCondition a condition an event has to satisfy to stop collecting events into * looping state * @return The same pattern with applied untilCondition */ public Pattern until(IterativeCondition untilCondition) { Preconditions.checkNotNull(untilCondition, "The condition cannot be null"); if (this.untilCondition != null) { throw new MalformedPatternException("Only one until condition can be applied."); } if (!quantifier.hasProperty(Quantifier.QuantifierProperty.LOOPING)) { throw new MalformedPatternException( "The until condition is only applicable to looping states."); } ClosureCleaner.clean(untilCondition, ExecutionConfig.ClosureCleanerLevel.RECURSIVE, true); this.untilCondition = untilCondition; return this; } /** * Defines the maximum time interval in which a matching pattern has to be completed in order to * be considered valid. This interval corresponds to the maximum time gap between first and the * last event. * * @param windowTime Time of the matching window * @return The same pattern operator with the new window length * @deprecated Use {@link #within(Duration)}. */ @Deprecated public Pattern within(@Nullable Time windowTime) { return within(Time.toDuration(windowTime)); } /** * Defines the maximum time interval in which a matching pattern has to be completed in order to * be considered valid. This interval corresponds to the maximum time gap between first and the * last event. * * @param windowTime Time of the matching window * @return The same pattern operator with the new window length */ public Pattern within(@Nullable Duration windowTime) { return within(windowTime, WithinType.FIRST_AND_LAST); } /** * Defines the maximum time interval in which a matching pattern has to be completed in order to * be considered valid. This interval corresponds to the maximum time gap between events. * * @param withinType Type of the within interval between events * @param windowTime Time of the matching window * @return The same pattern operator with the new window length * @deprecated Use {@link #within(Duration, WithinType)}. */ @Deprecated public Pattern within(@Nullable Time windowTime, WithinType withinType) { return within(Time.toDuration(windowTime), withinType); } /** * Defines the maximum time interval in which a matching pattern has to be completed in order to * be considered valid. This interval corresponds to the maximum time gap between events. * * @param withinType Type of the within interval between events * @param windowTime Time of the matching window * @return The same pattern operator with the new window length */ public Pattern within(@Nullable Duration windowTime, WithinType withinType) { if (windowTime != null) { windowTimes.put(withinType, windowTime); } return this; } /** * Appends a new pattern to the existing one. The new pattern enforces strict temporal * contiguity. This means that the whole pattern sequence matches only if an event which matches * this pattern directly follows the preceding matching event. Thus, there cannot be any events * in between two matching events. * * @param name Name of the new pattern * @return A new pattern which is appended to this one */ public Pattern next(final String name) { return new Pattern<>(name, this, ConsumingStrategy.STRICT, afterMatchSkipStrategy); } /** * Appends a new pattern to the existing one. The new pattern enforces that there is no event * matching this pattern right after the preceding matched event. * * @param name Name of the new pattern * @return A new pattern which is appended to this one */ public Pattern notNext(final String name) { if (quantifier.hasProperty(Quantifier.QuantifierProperty.OPTIONAL)) { throw new UnsupportedOperationException( "Specifying a pattern with an optional path to NOT condition is not supported yet. " + "You can simulate such pattern with two independent patterns, one with and the other without " + "the optional part."); } return new Pattern<>(name, this, ConsumingStrategy.NOT_NEXT, afterMatchSkipStrategy); } /** * Appends a new pattern to the existing one. The new pattern enforces non-strict temporal * contiguity. This means that a matching event of this pattern and the preceding matching event * might be interleaved with other events which are ignored. * * @param name Name of the new pattern * @return A new pattern which is appended to this one */ public Pattern followedBy(final String name) { return new Pattern<>(name, this, ConsumingStrategy.SKIP_TILL_NEXT, afterMatchSkipStrategy); } /** * Appends a new pattern to the existing one. The new pattern enforces that there is no event * matching this pattern between the preceding pattern and succeeding this one. * *

NOTE: There has to be other pattern after this one. * * @param name Name of the new pattern * @return A new pattern which is appended to this one */ public Pattern notFollowedBy(final String name) { if (quantifier.hasProperty(Quantifier.QuantifierProperty.OPTIONAL)) { throw new UnsupportedOperationException( "Specifying a pattern with an optional path to NOT condition is not supported yet. " + "You can simulate such pattern with two independent patterns, one with and the other without " + "the optional part."); } return new Pattern<>(name, this, ConsumingStrategy.NOT_FOLLOW, afterMatchSkipStrategy); } /** * Appends a new pattern to the existing one. The new pattern enforces non-strict temporal * contiguity. This means that a matching event of this pattern and the preceding matching event * might be interleaved with other events which are ignored. * * @param name Name of the new pattern * @return A new pattern which is appended to this one */ public Pattern followedByAny(final String name) { return new Pattern<>(name, this, ConsumingStrategy.SKIP_TILL_ANY, afterMatchSkipStrategy); } /** * Specifies that this pattern is optional for a final match of the pattern sequence to happen. * * @return The same pattern as optional. * @throws MalformedPatternException if the quantifier is not applicable to this pattern. */ public Pattern optional() { checkIfPreviousPatternGreedy(); quantifier.optional(); return this; } /** * Specifies that this pattern can occur {@code one or more} times. This means at least one and * at most infinite number of events can be matched to this pattern. * *

If this quantifier is enabled for a pattern {@code A.oneOrMore().followedBy(B)} and a * sequence of events {@code A1 A2 B} appears, this will generate patterns: {@code A1 B} and * {@code A1 A2 B}. See also {@link #allowCombinations()}. * * @return The same pattern with a {@link Quantifier#looping(ConsumingStrategy)} quantifier * applied. * @throws MalformedPatternException if the quantifier is not applicable to this pattern. */ public Pattern oneOrMore() { return oneOrMore((Duration) null); } /** * Specifies that this pattern can occur {@code one or more} times and time interval corresponds * to the maximum time gap between previous and current event for each times. This means at * least one and at most infinite number of events can be matched to this pattern. * *

If this quantifier is enabled for a pattern {@code A.oneOrMore().followedBy(B)} and a * sequence of events {@code A1 A2 B} appears, this will generate patterns: {@code A1 B} and * {@code A1 A2 B}. See also {@link #allowCombinations()}. * * @param windowTime time of the matching window between times * @return The same pattern with a {@link Quantifier#looping(ConsumingStrategy)} quantifier * applied. * @throws MalformedPatternException if the quantifier is not applicable to this pattern. * @deprecated Use {@link #oneOrMore(Duration)} */ @Deprecated public Pattern oneOrMore(@Nullable Time windowTime) { return oneOrMore(Time.toDuration(windowTime)); } /** * Specifies that this pattern can occur {@code one or more} times and time interval corresponds * to the maximum time gap between previous and current event for each times. This means at * least one and at most infinite number of events can be matched to this pattern. * *

If this quantifier is enabled for a pattern {@code A.oneOrMore().followedBy(B)} and a * sequence of events {@code A1 A2 B} appears, this will generate patterns: {@code A1 B} and * {@code A1 A2 B}. See also {@link #allowCombinations()}. * * @param windowTime time of the matching window between times * @return The same pattern with a {@link Quantifier#looping(ConsumingStrategy)} quantifier * applied. * @throws MalformedPatternException if the quantifier is not applicable to this pattern. */ public Pattern oneOrMore(@Nullable Duration windowTime) { checkIfNoNotPattern(); checkIfQuantifierApplied(); this.quantifier = Quantifier.looping(quantifier.getConsumingStrategy()); this.times = Times.of(1, windowTime); return this; } /** * Specifies that this pattern is greedy. This means as many events as possible will be matched * to this pattern. * * @return The same pattern with {@link Quantifier#greedy} set to true. * @throws MalformedPatternException if the quantifier is not applicable to this pattern. */ public Pattern greedy() { checkIfNoNotPattern(); checkIfNoGroupPattern(); this.quantifier.greedy(); return this; } /** * Specifies exact number of times that this pattern should be matched. * * @param times number of times matching event must appear * @return The same pattern with number of times applied * @throws MalformedPatternException if the quantifier is not applicable to this pattern. */ public Pattern times(int times) { return times(times, (Duration) null); } /** * Specifies exact number of times that this pattern should be matched and time interval * corresponds to the maximum time gap between previous and current event for each times. * * @param times number of times matching event must appear * @param windowTime time of the matching window between times * @return The same pattern with number of times applied * @throws MalformedPatternException if the quantifier is not applicable to this pattern. * @deprecated Using {@link #times(int, Duration)} */ @Deprecated public Pattern times(int times, @Nullable Time windowTime) { return times(times, Time.toDuration(windowTime)); } /** * Specifies exact number of times that this pattern should be matched and time interval * corresponds to the maximum time gap between previous and current event for each times. * * @param times number of times matching event must appear * @param windowTime time of the matching window between times * @return The same pattern with number of times applied * @throws MalformedPatternException if the quantifier is not applicable to this pattern. */ public Pattern times(int times, @Nullable Duration windowTime) { checkIfNoNotPattern(); checkIfQuantifierApplied(); Preconditions.checkArgument(times > 0, "You should give a positive number greater than 0."); this.quantifier = Quantifier.times(quantifier.getConsumingStrategy()); this.times = Times.of(times, windowTime); return this; } /** * Specifies that the pattern can occur between from and to times. * * @param from number of times matching event must appear at least * @param to number of times matching event must appear at most * @return The same pattern with the number of times range applied * @throws MalformedPatternException if the quantifier is not applicable to this pattern. */ public Pattern times(int from, int to) { return times(from, to, (Duration) null); } /** * Specifies that the pattern can occur between from and to times with time interval corresponds * to the maximum time gap between previous and current event for each times. * * @param from number of times matching event must appear at least * @param to number of times matching event must appear at most * @param windowTime time of the matching window between times * @return The same pattern with the number of times range applied * @throws MalformedPatternException if the quantifier is not applicable to this pattern. * @deprecated Use {@link #times(int, int, Duration)} */ @Deprecated public Pattern times(int from, int to, @Nullable Time windowTime) { return times(from, to, Time.toDuration(windowTime)); } /** * Specifies that the pattern can occur between from and to times with time interval corresponds * to the maximum time gap between previous and current event for each times. * * @param from number of times matching event must appear at least * @param to number of times matching event must appear at most * @param windowTime time of the matching window between times * @return The same pattern with the number of times range applied * @throws MalformedPatternException if the quantifier is not applicable to this pattern. */ public Pattern times(int from, int to, @Nullable Duration windowTime) { checkIfNoNotPattern(); checkIfQuantifierApplied(); this.quantifier = Quantifier.times(quantifier.getConsumingStrategy()); if (from == 0) { this.quantifier.optional(); from = 1; } this.times = Times.of(from, to, windowTime); return this; } /** * Specifies that this pattern can occur the specified times at least. This means at least the * specified times and at most infinite number of events can be matched to this pattern. * * @return The same pattern with a {@link Quantifier#looping(ConsumingStrategy)} quantifier * applied. * @throws MalformedPatternException if the quantifier is not applicable to this pattern. */ public Pattern timesOrMore(int times) { return timesOrMore(times, (Duration) null); } /** * Specifies that this pattern can occur the specified times at least with interval corresponds * to the maximum time gap between previous and current event for each times. This means at * least the specified times and at most infinite number of events can be matched to this * pattern. * * @param times number of times at least matching event must appear * @param windowTime time of the matching window between times * @return The same pattern with a {@link Quantifier#looping(ConsumingStrategy)} quantifier * applied. * @throws MalformedPatternException if the quantifier is not applicable to this pattern. * @deprecated Use {@link #timesOrMore(int, Duration)} */ @Deprecated public Pattern timesOrMore(int times, @Nullable Time windowTime) { return timesOrMore(times, Time.toDuration(windowTime)); } /** * Specifies that this pattern can occur the specified times at least with interval corresponds * to the maximum time gap between previous and current event for each times. This means at * least the specified times and at most infinite number of events can be matched to this * pattern. * * @param times number of times at least matching event must appear * @param windowTime time of the matching window between times * @return The same pattern with a {@link Quantifier#looping(ConsumingStrategy)} quantifier * applied. * @throws MalformedPatternException if the quantifier is not applicable to this pattern. */ public Pattern timesOrMore(int times, @Nullable Duration windowTime) { checkIfNoNotPattern(); checkIfQuantifierApplied(); this.quantifier = Quantifier.looping(quantifier.getConsumingStrategy()); this.times = Times.of(times, windowTime); return this; } /** * Applicable only to {@link Quantifier#looping(ConsumingStrategy)} and {@link * Quantifier#times(ConsumingStrategy)} patterns, this option allows more flexibility to the * matching events. * *

If {@code allowCombinations()} is not applied for a pattern {@code * A.oneOrMore().followedBy(B)} and a sequence of events {@code A1 A2 B} appears, this will * generate patterns: {@code A1 B} and {@code A1 A2 B}. If this method is applied, we will have * {@code A1 B}, {@code A2 B} and {@code A1 A2 B}. * * @return The same pattern with the updated quantifier. * * @throws MalformedPatternException if the quantifier is not applicable to this pattern. */ public Pattern allowCombinations() { quantifier.combinations(); return this; } /** * Works in conjunction with {@link Pattern#oneOrMore()} or {@link Pattern#times(int)}. * Specifies that any not matching element breaks the loop. * *

E.g. a pattern like: * *

{@code
     * Pattern.begin("start").where(new SimpleCondition() {
     *      @Override
     *      public boolean filter(Event value) throws Exception {
     *          return value.getName().equals("c");
     *      }
     * })
     * .followedBy("middle").where(new SimpleCondition() {
     *      @Override
     *      public boolean filter(Event value) throws Exception {
     *          return value.getName().equals("a");
     *      }
     * }).oneOrMore().consecutive()
     * .followedBy("end1").where(new SimpleCondition() {
     *      @Override
     *      public boolean filter(Event value) throws Exception {
     *          return value.getName().equals("b");
     *      }
     * });
     * }
* *

for a sequence: C D A1 A2 A3 D A4 B * *

will generate matches: {C A1 B}, {C A1 A2 B}, {C A1 A2 A3 B} * *

By default a relaxed continuity is applied. * * @return pattern with continuity changed to strict */ public Pattern consecutive() { quantifier.consecutive(); return this; } /** * Starts a new pattern sequence. The provided pattern is the initial pattern of the new * sequence. * * @param group the pattern to begin with * @param afterMatchSkipStrategy the {@link AfterMatchSkipStrategy.SkipStrategy} to use after * each match. * @return The first pattern of a pattern sequence */ public static GroupPattern begin( final Pattern group, final AfterMatchSkipStrategy afterMatchSkipStrategy) { return new GroupPattern<>(null, group, ConsumingStrategy.STRICT, afterMatchSkipStrategy); } /** * Starts a new pattern sequence. The provided pattern is the initial pattern of the new * sequence. * * @param group the pattern to begin with * @return the first pattern of a pattern sequence */ public static GroupPattern begin(Pattern group) { return new GroupPattern<>( null, group, ConsumingStrategy.STRICT, AfterMatchSkipStrategy.noSkip()); } /** * Appends a new group pattern to the existing one. The new pattern enforces non-strict temporal * contiguity. This means that a matching event of this pattern and the preceding matching event * might be interleaved with other events which are ignored. * * @param group the pattern to append * @return A new pattern which is appended to this one */ public GroupPattern followedBy(Pattern group) { return new GroupPattern<>( this, group, ConsumingStrategy.SKIP_TILL_NEXT, afterMatchSkipStrategy); } /** * Appends a new group pattern to the existing one. The new pattern enforces non-strict temporal * contiguity. This means that a matching event of this pattern and the preceding matching event * might be interleaved with other events which are ignored. * * @param group the pattern to append * @return A new pattern which is appended to this one */ public GroupPattern followedByAny(Pattern group) { return new GroupPattern<>( this, group, ConsumingStrategy.SKIP_TILL_ANY, afterMatchSkipStrategy); } /** * Appends a new group pattern to the existing one. The new pattern enforces strict temporal * contiguity. This means that the whole pattern sequence matches only if an event which matches * this pattern directly follows the preceding matching event. Thus, there cannot be any events * in between two matching events. * * @param group the pattern to append * @return A new pattern which is appended to this one */ public GroupPattern next(Pattern group) { return new GroupPattern<>(this, group, ConsumingStrategy.STRICT, afterMatchSkipStrategy); } private void checkIfNoNotPattern() { if (quantifier.getConsumingStrategy() == ConsumingStrategy.NOT_FOLLOW || quantifier.getConsumingStrategy() == ConsumingStrategy.NOT_NEXT) { throw new MalformedPatternException("Option not applicable to NOT pattern"); } } private void checkIfQuantifierApplied() { if (!quantifier.hasProperty(Quantifier.QuantifierProperty.SINGLE)) { throw new MalformedPatternException( "Already applied quantifier to this Pattern. " + "Current quantifier is: " + quantifier); } } /** @return the pattern's {@link AfterMatchSkipStrategy.SkipStrategy} after match. */ public AfterMatchSkipStrategy getAfterMatchSkipStrategy() { return afterMatchSkipStrategy; } private void checkIfNoGroupPattern() { if (this instanceof GroupPattern) { throw new MalformedPatternException("Option not applicable to group pattern"); } } private void checkIfPreviousPatternGreedy() { if (previous != null && previous.getQuantifier().hasProperty(Quantifier.QuantifierProperty.GREEDY)) { throw new MalformedPatternException( "Optional pattern cannot be preceded by greedy pattern"); } } @Override public String toString() { return "Pattern{" + "name='" + name + '\'' + ", previous=" + previous + ", condition=" + condition + ", windowTimes=" + windowTimes + ", quantifier=" + quantifier + ", untilCondition=" + untilCondition + ", times=" + times + ", afterMatchSkipStrategy=" + afterMatchSkipStrategy + '}'; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy