Skip to content

Commit 9c6ab94

Browse files
committed
WIP
1 parent 8f815f0 commit 9c6ab94

File tree

4 files changed

+386
-1
lines changed

4 files changed

+386
-1
lines changed
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
/**
2+
* Copyright 2018 Inscope Metrics, Inc
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.arpnetworking.utility;
17+
18+
import scala.Int;
19+
20+
import java.util.Map;
21+
import java.util.regex.Matcher;
22+
23+
/**
24+
* A regex replacement utility that can also replace tokens not found in the regex.
25+
*
26+
* @author Brandon Arp (brandon dot arp at inscopemetrics dot com)
27+
*/
28+
public final class RegexAndMapReplacer {
29+
/**
30+
* Replaces all instances of $n (where n is 0-9) with regex match groups, ${var} with regex capture group or variable (from map) 'var'.
31+
*
32+
* @param matcher matcher to use
33+
* @param input input string to match against
34+
* @param replace replacement string
35+
* @param variables map of variables to include
36+
* @return
37+
*/
38+
public static String replaceAll(final Matcher matcher, final String input, final String replace, final Map<String, String> variables) {
39+
40+
matcher.reset();
41+
boolean found = matcher.find();
42+
if (found) {
43+
final StringBuilder builder = new StringBuilder();
44+
int lastMatchedIndex = 0;
45+
do {
46+
builder.append(input.substring(lastMatchedIndex, matcher.start()));
47+
lastMatchedIndex = matcher.end();
48+
appendReplacement(matcher, replace, builder, variables);
49+
found = matcher.find();
50+
} while (found);
51+
// Append left-over string after the matches
52+
if (lastMatchedIndex < input.length() - 1) {
53+
builder.append(input.substring(lastMatchedIndex, input.length()));
54+
}
55+
return builder.toString();
56+
}
57+
return input;
58+
}
59+
60+
private static void appendReplacement(final Matcher matcher, final String replacement, final StringBuilder replacementBuilder,
61+
final Map<String, String> variables) {
62+
final StringBuilder tokenStringBuilder = new StringBuilder();
63+
boolean inEscape = false;
64+
boolean readingReplaceToken = false;
65+
boolean inReplaceBrackets = false;
66+
boolean replaceTokenNumeric = true; // assume token is numeric, any non-numeric character will set this to false
67+
for (int x = 0; x < replacement.length(); x++) {
68+
final char c = replacement.charAt(x);
69+
if (!inEscape && c == '\\') {
70+
inEscape = true;
71+
continue;
72+
}
73+
if (inEscape) {
74+
final StringBuilder builder;
75+
if (readingReplaceToken) {
76+
builder = tokenStringBuilder;
77+
} else {
78+
builder = replacementBuilder;
79+
}
80+
if (c == '\\' || c == '$' || c == '}') {
81+
builder.append(c);
82+
inEscape = false;
83+
} else {
84+
throw new IllegalArgumentException("Improperly escaped '" + String.valueOf(c) + "' in replacement at col " + x + ": " + replacement);
85+
}
86+
} else if (readingReplaceToken) {
87+
if (c == '{') {
88+
inReplaceBrackets = true;
89+
continue;
90+
} else if (c == '}' && inReplaceBrackets) {
91+
final String replaceToken = tokenStringBuilder.toString();
92+
replacementBuilder.append(getReplacement(matcher, replaceToken, replaceTokenNumeric, variables));
93+
tokenStringBuilder.setLength(0);
94+
inReplaceBrackets = false;
95+
readingReplaceToken = false;
96+
continue;
97+
} else if (!Character.isDigit(c)) {
98+
if (inReplaceBrackets) {
99+
replaceTokenNumeric = false;
100+
tokenStringBuilder.append(c);
101+
} else {
102+
final String replaceToken = tokenStringBuilder.toString();
103+
if (replaceToken.isEmpty()) {
104+
throw new IllegalArgumentException("Non-numeric replacements must be of the form ${val}. Missing '{' at col "
105+
+ x + ": " + replacement);
106+
}
107+
replacementBuilder.append(getReplacement(matcher, replaceToken, replaceTokenNumeric, variables));
108+
tokenStringBuilder.setLength(0);
109+
readingReplaceToken = false;
110+
x--; // We can't process the character because we are no longer in the numeric group syntax, we need to process the
111+
// $n replacement and then evaluate this character again.
112+
continue;
113+
}
114+
} else {
115+
tokenStringBuilder.append(c);
116+
}
117+
} else {
118+
if (c == '$') {
119+
readingReplaceToken = true;
120+
} else {
121+
replacementBuilder.append(c);
122+
}
123+
}
124+
}
125+
if (tokenStringBuilder.length() > 0 && replaceTokenNumeric) {
126+
final String replaceToken = tokenStringBuilder.toString();
127+
replacementBuilder.append(getReplacement(matcher, replaceToken, true, variables));
128+
}
129+
}
130+
131+
private static String getReplacement(final Matcher matcher, final String replaceToken, final boolean numeric,
132+
final Map<String, String> variables) {
133+
if (numeric) {
134+
final int replaceGroup = Integer.parseInt(replaceToken);
135+
return matcher.group(replaceGroup);
136+
} else {
137+
try {
138+
return matcher.group(replaceToken);
139+
} catch (final IllegalArgumentException e) { // No group with this name
140+
return variables.getOrDefault(replaceToken, "");
141+
}
142+
}
143+
}
144+
145+
private RegexAndMapReplacer () { }
146+
}

src/test/java/com/arpnetworking/metrics/mad/sources/MappingSourceTest.java

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,9 @@ public void setUp() {
5252
.setName("MergingSourceTest")
5353
.setFindAndReplace(ImmutableMap.of(
5454
"foo/([^/]*)/bar", ImmutableList.of("foo/bar"),
55-
"cat/([^/]*)/dog", ImmutableList.of("cat/dog", "cat/dog/$1")))
55+
"cat/([^/]*)/dog", ImmutableList.of("cat/dog", "cat/dog/$1"),
56+
"tagged/([^/]*)/dog", ImmutableList.of("tagged/dog;animal=$1"),
57+
"tagged/([^/]*)/dog", ImmutableList.of("tagged/$animal/dog")))
5658
.setSource(_mockSource);
5759
}
5860

@@ -278,6 +280,48 @@ public void testReplaceWithCapture() {
278280
UnorderedRecordEquality.equals(expectedRecord, actualRecord));
279281
}
280282

283+
@Test
284+
public void testReplaceWithCaptureWithTags() {
285+
final Record matchingRecord = TestBeanFactory.createRecordBuilder()
286+
.setMetrics(ImmutableMap.of(
287+
"cat/sheep/dog",
288+
TestBeanFactory.createMetricBuilder()
289+
.setType(MetricType.GAUGE)
290+
.setValues(ImmutableList.of(
291+
new Quantity.Builder()
292+
.setValue(1.23d)
293+
.setUnit(Unit.BYTE)
294+
.build()))
295+
.build()))
296+
.build();
297+
298+
final Source mergingSource = _mappingSourceBuilder.build();
299+
mergingSource.attach(_mockObserver);
300+
notify(_mockSource, matchingRecord);
301+
302+
final ArgumentCaptor<Record> argument = ArgumentCaptor.forClass(Record.class);
303+
Mockito.verify(_mockObserver).notify(Mockito.same(mergingSource), argument.capture());
304+
final Record actualRecord = argument.getValue();
305+
306+
final Record expectedRecord = TestBeanFactory.createRecordBuilder()
307+
.setAnnotations(matchingRecord.getAnnotations())
308+
.setTime(matchingRecord.getTime())
309+
.setMetrics(ImmutableMap.of(
310+
"cat/dog/sheep",
311+
TestBeanFactory.createMetricBuilder()
312+
.setType(MetricType.GAUGE)
313+
.setValues(ImmutableList.of(
314+
new Quantity.Builder()
315+
.setValue(1.23d)
316+
.setUnit(Unit.BYTE)
317+
.build()))
318+
.build()))
319+
.build();
320+
Assert.assertTrue(
321+
String.format("expected=%s, actual=%s", expectedRecord, actualRecord),
322+
UnorderedRecordEquality.equals(expectedRecord, actualRecord));
323+
}
324+
281325
@Test
282326
public void testMultipleMatches() {
283327
final Record matchingRecord = TestBeanFactory.createRecordBuilder()
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package com.arpnetworking.utility;
2+
3+
import com.carrotsearch.junitbenchmarks.BenchmarkOptions;
4+
import com.carrotsearch.junitbenchmarks.BenchmarkRule;
5+
import com.google.common.collect.ImmutableMap;
6+
import org.junit.Rule;
7+
import org.junit.Test;
8+
import org.junit.rules.TestRule;
9+
10+
import java.util.Collections;
11+
import java.util.regex.Pattern;
12+
13+
public class RegexAndMapBenchmarkTest {
14+
@Rule
15+
public TestRule benchmarkRun = new BenchmarkRule();
16+
17+
@BenchmarkOptions(benchmarkRounds = 2000000, warmupRounds = 50000)
18+
@Test
19+
public void testRegexAndMap() {
20+
final String result = RegexAndMapReplacer.replaceAll(PATTERN.matcher(INPUT), INPUT, REPLACE, Collections.emptyMap());
21+
}
22+
23+
@BenchmarkOptions(benchmarkRounds = 2000000, warmupRounds = 50000)
24+
@Test
25+
public void testRegex() {
26+
final String result = PATTERN.matcher(INPUT).replaceAll(REPLACE);
27+
}
28+
29+
private static final String REPLACE = "this is a ${g1} pattern called ${g2}";
30+
private static Pattern PATTERN = Pattern.compile("(?<g1>test)/pattern/(?<g2>foo)");
31+
private static final String INPUT = "test/pattern/foo";
32+
}

0 commit comments

Comments
 (0)