1616package com .arpnetworking .utility ;
1717
1818import com .google .common .collect .ImmutableList ;
19+ import com .google .common .collect .ImmutableMap ;
20+ import com .google .common .collect .Maps ;
1921
2022import java .util .LinkedHashMap ;
2123import java .util .Map ;
2224import java .util .regex .Matcher ;
2325import java .util .regex .Pattern ;
26+ import java .util .stream .Collectors ;
2427
2528/**
2629 * A regex replacement utility that can also replace tokens not found in the regex.
2730 *
28- * $n where n is a number in the replace string is replaced by the pattern's match group n
2931 * ${name} in the replace string is replaced by the pattern's named capture, or by the value of the variable
3032 * with that name from the variables map
3133 * \ is used as an escape character and may be used to escape '$', '{', and '}' characters so that they will
@@ -43,38 +45,44 @@ public final class RegexAndMapReplacer {
4345 * @param variables map of variables to include
4446 * @return a string with replacement tokens replaced
4547 */
46- public static Replacement replaceAll (
48+ public static Replacement replaceAll (
4749 final Pattern pattern ,
4850 final String input ,
4951 final String replace ,
5052 final LinkedHashMap <String , Map <String , String >> variables ) {
5153 final Matcher matcher = pattern .matcher (input );
5254 boolean found = matcher .find ();
5355 if (found ) {
54- final ImmutableList .Builder <String > variablesUsedBuilder = ImmutableList . builder ();
56+ final Map < String , ImmutableList .Builder <String >> variablesUsed = Maps . newHashMap ();
5557 final StringBuilder builder = new StringBuilder ();
5658 int lastMatchedIndex = 0 ;
5759 do {
5860 builder .append (input .substring (lastMatchedIndex , matcher .start ()));
5961 lastMatchedIndex = matcher .end ();
60- appendReplacement (matcher , replace , builder , variables , variablesUsedBuilder );
62+ appendReplacement (matcher , replace , builder , variables , variablesUsed );
6163 found = matcher .find ();
6264 } while (found );
6365 // Append left-over string after the matches
6466 if (lastMatchedIndex < input .length () - 1 ) {
6567 builder .append (input .substring (lastMatchedIndex , input .length ()));
6668 }
67- return new Replacement (builder .toString (), variablesUsedBuilder .build ());
69+ final ImmutableMap <String , ImmutableList <String >> immutableVars = variablesUsed .entrySet ()
70+ .stream ()
71+ .collect (
72+ Collectors .collectingAndThen (
73+ Collectors .toMap (ImmutableMap .Entry ::getKey , entry -> entry .getValue ().build ()),
74+ ImmutableMap ::copyOf ));
75+ return new Replacement (builder .toString (), immutableVars );
6876 }
69- return new Replacement (input , ImmutableList .of ());
77+ return new Replacement (input , ImmutableMap .of ());
7078 }
7179
7280 private static void appendReplacement (
7381 final Matcher matcher ,
7482 final String replacement ,
7583 final StringBuilder replacementBuilder ,
7684 final LinkedHashMap <String , Map <String , String >> variables ,
77- final ImmutableList .Builder <String > variablesUsedBuilder ) {
85+ final Map < String , ImmutableList .Builder <String >> variablesUsed ) {
7886 final StringBuilder tokenBuilder = new StringBuilder ();
7987 int x = -1 ;
8088 while (x < replacement .length () - 1 ) {
@@ -85,7 +93,7 @@ private static void appendReplacement(
8593 processEscapedCharacter (replacement , x , replacementBuilder );
8694 } else {
8795 if (c == '$' ) {
88- x += writeReplacementToken (replacement , x , replacementBuilder , matcher , variables , tokenBuilder , variablesUsedBuilder );
96+ x += writeReplacementToken (replacement , x , replacementBuilder , matcher , variables , tokenBuilder , variablesUsed );
8997 } else {
9098 replacementBuilder .append (c );
9199 }
@@ -114,7 +122,7 @@ private static int writeReplacementToken(
114122 final Matcher matcher ,
115123 final LinkedHashMap <String , Map <String , String >> variables ,
116124 final StringBuilder tokenBuilder ,
117- final ImmutableList .Builder <String > variablesUsedBuilder ) {
125+ final Map < String , ImmutableList .Builder <String >> variablesUsed ) {
118126 boolean inReplaceBrackets = false ;
119127 boolean tokenNumeric = true ;
120128 tokenBuilder .setLength (0 ); // reset the shared builder
@@ -147,85 +155,68 @@ private static int writeReplacementToken(
147155 throw new IllegalArgumentException ("Invalid replacement token, expected '}' at col " + x + ": " + replacement );
148156 }
149157 x ++; // Consume the }
150- output .append (getReplacement (matcher , tokenBuilder .toString (), tokenNumeric , variables , variablesUsedBuilder ));
158+ output .append (getReplacement (matcher , tokenBuilder .toString (), variables , variablesUsed ));
151159 } else {
152- // Consume until we hit a non-digit character
153- while (x < replacement .length ()) {
154- c = replacement .charAt (x );
155- if (Character .isDigit (c )) {
156- tokenBuilder .append (c );
157- } else {
158- break ;
159- }
160- x ++;
161- }
162- if (tokenBuilder .length () == 0 ) {
163160 throw new IllegalArgumentException (
164161 String .format (
165- "Invalid replacement token, non-numeric tokens must be surrounded by { } at col %d: %s" ,
162+ "Invalid replacement token, tokens must be surrounded by { } at col %d: %s" ,
166163 x ,
167164 replacement ));
168- }
169- output .append (getReplacement (matcher , tokenBuilder .toString (), true , variables , variablesUsedBuilder ));
170165 }
171166 return x - offset - 1 ;
172167 }
173168
174169 private static String getReplacement (
175170 final Matcher matcher ,
176171 final String replaceToken ,
177- final boolean numeric ,
178172 final LinkedHashMap <String , Map <String , String >> variables ,
179- final ImmutableList .Builder <String > variablesUsedBuilder ) {
180- if (numeric ) {
181- final int replaceGroup = Integer .parseInt (replaceToken );
182- return matcher .group (replaceGroup );
173+ final Map <String , ImmutableList .Builder <String >> variablesUsed ) {
174+ final int prefixSeparatorIndex = replaceToken .indexOf (':' );
175+ final String prefix ;
176+ final String variableName ;
177+ if (prefixSeparatorIndex == replaceToken .length () - 1 ) {
178+ throw new IllegalArgumentException (
179+ String .format ("found prefix in variable replacement, but no variable name found: '%s'" ,
180+ replaceToken ));
181+ }
182+ if (prefixSeparatorIndex != -1 ) {
183+ prefix = replaceToken .substring (0 , prefixSeparatorIndex );
184+ variableName = replaceToken .substring (prefixSeparatorIndex + 1 );
183185 } else {
184- final int prefixSeparatorIndex = replaceToken .indexOf (':' );
185- final String prefix ;
186- final String variableName ;
187- if (prefixSeparatorIndex == replaceToken .length () - 1 ) {
188- throw new IllegalArgumentException (
189- String .format ("found prefix in variable replacement, but no variable name found: '%s'" ,
190- replaceToken ));
191- }
192- if (prefixSeparatorIndex != -1 ) {
193- prefix = replaceToken .substring (0 , prefixSeparatorIndex );
194- variableName = replaceToken .substring (prefixSeparatorIndex + 1 );
195- } else {
196- prefix = "" ;
197- variableName = replaceToken ;
198- }
199- if (prefix .isEmpty ()) {
200- return replaceNonPrefix (matcher , variables , variablesUsedBuilder , variableName );
186+ prefix = "" ;
187+ variableName = replaceToken ;
188+ }
189+ if (prefix .isEmpty ()) {
190+ return replaceNonPrefix (matcher , variables , variablesUsed , variableName );
201191
202- } else {
203- return replacePrefix (matcher , variables , variablesUsedBuilder , prefix , variableName );
204- }
192+ } else {
193+ return replacePrefix (matcher , variables , variablesUsed , prefix , variableName );
205194 }
206195 }
207196
208197 private static String replacePrefix (
209198 final Matcher matcher ,
210199 final LinkedHashMap <String , Map <String , String >> variables ,
211- final ImmutableList .Builder <String > variablesUsedBuilder ,
200+ final Map < String , ImmutableList .Builder <String >> variablesUsed ,
212201 final String prefix ,
213202 final String variableName ) {
214203 if (prefix .equals ("capture" )) {
215204 if (isNumeric (variableName )) {
216205 final Integer groupIndex = Integer .valueOf (variableName );
217- return matcher .group (groupIndex );
218- } else {
219- try {
220- return matcher .group (variableName );
221- } catch (final IllegalArgumentException ignored2 ) { // No group with this name
222- return "" ;
206+ if (groupIndex >= 0 && groupIndex <= matcher .groupCount ()) {
207+ return matcher .group (groupIndex );
223208 }
224209 }
210+
211+ try {
212+ return matcher .group (variableName );
213+ } catch (final IllegalArgumentException ignored ) { // No group with this name
214+ return "" ;
215+ }
225216 }
226217
227218 // Only record variables that are not captures
228- variablesUsedBuilder . add ( String . format ( "%s:%s" , prefix , variableName ) );
219+ variablesUsed . computeIfAbsent ( prefix , key -> ImmutableList . builder ()). add ( variableName );
229220
230221 final Map <String , String > variableMap = variables .get (prefix );
231222 if (variableMap == null ) {
@@ -237,9 +228,16 @@ private static String replacePrefix(
237228 private static String replaceNonPrefix (
238229 final Matcher matcher ,
239230 final LinkedHashMap <String , Map <String , String >> variables ,
240- final ImmutableList .Builder <String > variablesUsedBuilder ,
231+ final Map < String , ImmutableList .Builder <String >> variablesUsed ,
241232 final String variableName ) {
242- // First try the capture group with the name
233+ // First try to check against the capture group number
234+ if (isNumeric (variableName )) {
235+ final int replaceGroup = Integer .parseInt (variableName );
236+ if (replaceGroup >= 0 && replaceGroup <= matcher .groupCount ()) {
237+ return matcher .group (replaceGroup );
238+ }
239+ }
240+ // Then try the capture group with the name
243241 try {
244242 return matcher .group (variableName );
245243 } catch (final IllegalArgumentException e ) { // No group with this name
@@ -248,7 +246,7 @@ private static String replaceNonPrefix(
248246 final Map <String , String > variableMap = entry .getValue ();
249247 final String replacement = variableMap .get (variableName );
250248 if (replacement != null ) {
251- variablesUsedBuilder . add ( String . format ( "%s:%s" , entry .getKey (), variableName ) );
249+ variablesUsed . computeIfAbsent ( entry .getKey (), key -> ImmutableList . builder ()). add ( variableName );
252250 return replacement ;
253251 }
254252 }
@@ -280,17 +278,17 @@ public String getReplacement() {
280278 return _replacement ;
281279 }
282280
283- public ImmutableList <String > getVariablesMatched () {
281+ public ImmutableMap < String , ImmutableList <String > > getVariablesMatched () {
284282 return _variablesMatched ;
285283 }
286284
287- private Replacement (final String replacement , final ImmutableList <String > variablesMatched ) {
285+ private Replacement (final String replacement , final ImmutableMap < String , ImmutableList <String > > variablesMatched ) {
288286
289287 _replacement = replacement ;
290288 _variablesMatched = variablesMatched ;
291289 }
292290
293291 private final String _replacement ;
294- private final ImmutableList <String > _variablesMatched ;
292+ private final ImmutableMap < String , ImmutableList <String > > _variablesMatched ;
295293 }
296294}
0 commit comments