Java PatternSyntaxException: Fixing 'Illegal Repetition' Error in String Token Substitution with Maps
String token substitution is a common task in Java development, often used for dynamic template rendering, configuration parsing, or generating user messages. A typical approach involves replacing placeholders (tokens) in a template string with values from a Map (e.g., replacing {name} with "Alice" or {orderId} with "12345"). However, developers frequently encounter the PatternSyntaxException: Illegal repetition error when performing this substitution—especially when tokens contain special characters.
This blog post dives into the root cause of this error, explains why it occurs when using Map keys for token substitution, and provides step-by-step solutions to fix it. We’ll also cover advanced considerations and best practices to avoid similar issues in the future.
Table of Contents#
- Understanding the 'Illegal Repetition' Error
- Common Scenario: String Token Substitution with Maps
- Root Cause Analysis
- Step-by-Step Fixes
- Advanced Considerations
- Conclusion
- References
Understanding the 'Illegal Repetition' Error#
The PatternSyntaxException is a checked exception thrown when a regular expression (regex) pattern is invalid. The "Illegal repetition" message specifically occurs when the regex engine encounters an invalid repetition quantifier. Repetition quantifiers in regex include * (zero or more), + (one or more), ? (zero or one), and {n,m} (between n and m times).
For example:
a{is invalid (unclosed{with no numbers).*abcis invalid (*has no preceding element to repeat).user{idis invalid ({idis not a valid quantifier like{2}or{1,3}).
Common Scenario: String Token Substitution with Maps#
A typical workflow for token substitution with a Map looks like this:
- Define a template string with tokens (e.g.,
"Hello {name}, your order {order*} is ready!"). - Use a
Mapto store token-value pairs (e.g.,{ "{name}": "Alice", "{order*}": "12345" }). - Iterate over the
Mapand replace each token in the template with its corresponding value.
Developers often use String.replaceAll(String regex, String replacement) for substitution, assuming the token is a literal string. However, this is where the "Illegal repetition" error often strikes.
Example Code (That Fails)#
Suppose we have the following template and Map:
String template = "Hello {name}, your order {order*} is ready!";
Map<String, String> tokens = new HashMap<>();
tokens.put("{name}", "Alice");
tokens.put("{order*}", "12345"); // Token contains "*"
// Attempt to replace tokens using replaceAll()
for (Map.Entry<String, String> entry : tokens.entrySet()) {
template = template.replaceAll(entry.getKey(), entry.getValue());
} Result:
Exception in thread "main" java.util.regex.PatternSyntaxException: Illegal repetition near index 0
{order*}
^
Root Cause Analysis: Why Maps and Regex Don’t Mix#
The error occurs because String.replaceAll() treats the first argument as a regex pattern, not a literal string. When you pass a Map key like "{order*}" directly to replaceAll(), the regex engine interprets special characters (e.g., {, }, *) as regex syntax instead of literal text.
{and}: Define repetition quantifiers (e.g.,a{2}matches "aa").*: Matches zero or more occurrences of the preceding element.
In the example, the token "{order*}" is parsed as a regex pattern. The { at the start of {order*} is interpreted as the beginning of a repetition quantifier, but there’s no valid number (e.g., {2}) or closing }, leading to "Illegal repetition."
Step-by-Step Fixes#
To resolve the "Illegal repetition" error, we need to ensure token keys are treated as literal strings, not regex patterns. Below are proven solutions:
Fix 1: Escape Regex Metacharacters with Pattern.quote()#
The Pattern.quote(String s) method escapes all regex metacharacters in s, ensuring it is treated as a literal in regex operations. This is the most reliable fix when using replaceAll().
How It Works:
Pattern.quote(s) wraps s in \Q and \E (regex escape sequences), which tell the regex engine to treat the enclosed text as a literal.
Corrected Code:
import java.util.regex.Pattern;
// ...
for (Map.Entry<String, String> entry : tokens.entrySet()) {
String token = entry.getKey();
String escapedToken = Pattern.quote(token); // Escape regex special chars
String value = entry.getValue();
template = template.replaceAll(escapedToken, value);
} Why This Works:
Pattern.quote("{order*}") converts the token to \Q{order*}\E, which the regex engine interprets as the literal string {order*}.
Fix 2: Use String.replace() for Literal Substitution#
String.replace(CharSequence target, CharSequence replacement) treats target as a literal string (not regex) and replaces all occurrences. This avoids regex parsing entirely.
How It Works:
Unlike replaceAll(), replace() does not interpret the target as a regex pattern. It directly matches and replaces literal substrings.
Corrected Code:
for (Map.Entry<String, String> entry : tokens.entrySet()) {
template = template.replace(entry.getKey(), entry.getValue());
} Why This Works:
replace("{order*}", "12345") searches for the literal substring {order*} and replaces it with "12345", no regex parsing involved.
Fix 3: Leverage Libraries Like Apache Commons Text#
For complex substitution (e.g., nested tokens, dynamic values), use libraries like Apache Commons Text’s StringSubstitutor, which automatically handles escaping and regex edge cases.
Steps to Use:
-
Add the Apache Commons Text dependency (Maven):
<dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-text</artifactId> <version>1.10.0</version> <!-- Check for latest version --> </dependency> -
Use
StringSubstitutorto replace tokens:import org.apache.commons.text.StringSubstitutor; // ... StringSubstitutor substitutor = new StringSubstitutor(tokens); String result = substitutor.replace(template); System.out.println(result); // Output: "Hello Alice, your order 12345 is ready!"
Why This Works:
StringSubstitutor treats tokens as literals by default and handles escaping of special characters internally, eliminating the need for manual regex quoting.
Bonus: Handle Replacement Strings with Special Characters#
If your replacement values contain regex metacharacters (e.g., $, \), use Matcher.quoteReplacement(String s) to escape them. This prevents issues where $1 in the replacement is misinterpreted as a regex group reference.
Example:
import java.util.regex.Matcher;
// ...
for (Map.Entry<String, String> entry : tokens.entrySet()) {
String escapedToken = Pattern.quote(entry.getKey());
String escapedValue = Matcher.quoteReplacement(entry.getValue()); // Escape replacement
template = template.replaceAll(escapedToken, escapedValue);
} Advanced Considerations#
1. Performance with Large Maps#
For large Maps (1000+ entries), repeatedly modifying the template string with replace() or replaceAll() creates multiple intermediate String objects (since strings are immutable in Java). Use StringBuilder for efficiency:
StringBuilder sb = new StringBuilder(template);
for (Map.Entry<String, String> entry : tokens.entrySet()) {
String token = entry.getKey();
String value = entry.getValue();
int index = sb.indexOf(token);
while (index != -1) {
sb.replace(index, index + token.length(), value);
index = sb.indexOf(token, index + value.length());
}
}
String result = sb.toString(); 2. Dynamic or Nested Tokens#
If tokens are dynamic (e.g., generated at runtime) or nested (e.g., {user.{id}}), use StringSubstitutor with custom delimiters to avoid conflicts:
StringSubstitutor substitutor = new StringSubstitutor(tokens, "{{", "}}"); // Use {{token}} instead of {token} 3. Validation of Tokens#
Pre-validate Map keys to ensure they don’t contain invalid characters (e.g., unescaped { or }) if using custom substitution logic.
Conclusion#
The "Illegal repetition" PatternSyntaxException occurs when token keys with regex special characters (e.g., {, }, *) are passed directly to replaceAll(). To fix this:
- Use
Pattern.quote()to escape tokens when usingreplaceAll(). - Use
String.replace()for simple literal substitution. - For complex scenarios, use libraries like Apache Commons Text’s
StringSubstitutor.
By treating tokens as literals (not regex patterns), you ensure reliable and error-free string substitution with Maps.