/
Placeholders.java
236 lines (211 loc) · 10.8 KB
/
Placeholders.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
/*
* Copyright (c) 2017 Contributors to the Eclipse Foundation
*
* See the NOTICE file(s) distributed with this work for additional
* information regarding copyright ownership.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.ditto.model.base.common;
import static java.util.Objects.requireNonNull;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.annotation.concurrent.Immutable;
import org.eclipse.ditto.model.base.exceptions.DittoRuntimeException;
/**
* Supports substitution of placeholders in the format {@code {{ prefix:key }}}
* or the legacy-format {@code ${prefix.key}}.
*/
@Immutable
public final class Placeholders {
private static final String PLACEHOLDER_GROUP_NAME = "ph";
private static final String PLACEHOLDER_START = "\\{{2}(?!\\s*\\{)";
private static final String PLACEHOLDER_END = "}}";
private static final String PLACEHOLDER_GROUP = "(?<" + PLACEHOLDER_GROUP_NAME + ">(([^}]|}[^}])*+))";
private static final String LEGACY_PLACEHOLDER_GROUP = "(?<" + PLACEHOLDER_GROUP_NAME + ">([^}]*+))";
private static final String ANY_NUMBER_OF_SPACES = "\\s*+";
private static final String PLACEHOLDER_REGEX = PLACEHOLDER_START
+ ANY_NUMBER_OF_SPACES // allow arbitrary number of spaces
+ PLACEHOLDER_GROUP // the content of the placeholder
+ ANY_NUMBER_OF_SPACES // allow arbitrary number of spaces
+ Pattern.quote(PLACEHOLDER_END); // end of placeholder
private static final Pattern PLACEHOLDER_PATTERN = Pattern.compile(PLACEHOLDER_REGEX);
private static final String LEGACY_PLACEHOLDER_START = "${";
private static final String LEGACY_PLACEHOLDER_END = "}";
private static final String LEGACY_PLACEHOLDER_REGEX =
Pattern.quote(LEGACY_PLACEHOLDER_START) + LEGACY_PLACEHOLDER_GROUP + Pattern.quote(LEGACY_PLACEHOLDER_END);
private static final Pattern LEGACY_PLACEHOLDER_PATTERN = Pattern.compile(LEGACY_PLACEHOLDER_REGEX);
private static final String LEGACY_REQUEST_SUBJECT_ID =
"(?<" + PLACEHOLDER_GROUP_NAME + ">" + Pattern.quote("request.subjectId") + ")";
private static final String LEGACY_REQUEST_SUBJECT_ID_REGEX =
Pattern.quote(LEGACY_PLACEHOLDER_START) + LEGACY_REQUEST_SUBJECT_ID + Pattern.quote(LEGACY_PLACEHOLDER_END);
private static final Pattern LEGACY_REQUEST_SUBJECT_ID_PATTERN = Pattern.compile(LEGACY_REQUEST_SUBJECT_ID_REGEX);
private Placeholders() {
throw new AssertionError();
}
/**
* Checks whether the given {@code input} contains any placeholder.
*
* @param input the input.
* @return {@code} true, if the input contains a placeholder.
*/
public static boolean containsAnyPlaceholder(final CharSequence input) {
requireNonNull(input);
return containsPlaceholder(input) || containsLegacyPlaceholder(input);
}
/**
* Checks whether the given {@code input} contains any placeholder.
*
* @param input the input.
* @return {@code} true, if the input contains a placeholder.
*/
private static boolean containsPlaceholder(final CharSequence input) {
requireNonNull(input);
return PLACEHOLDER_PATTERN.matcher(input).find();
}
/**
* Checks whether the given {@code input} contains any legacy placeholder.
*
* @param input the input.
* @return {@code} true, if the input contains a placeholder.
*/
private static boolean containsLegacyPlaceholder(final CharSequence input) {
requireNonNull(input);
return LEGACY_PLACEHOLDER_PATTERN.matcher(input).find();
}
/**
* Checks whether the given {@code input} contains legacy request subject id placeholder.
*
* @param input the input.
* @return {@code} true, if the input contains a placeholder.
*/
private static boolean containsLegacyRequestSubjectIdPlaceholder(final CharSequence input) {
requireNonNull(input);
return LEGACY_REQUEST_SUBJECT_ID_PATTERN.matcher(input).find();
}
/**
* Substitutes any placeholder contained in the input not allowing unresolved placeholders.
*
* @param input the input.
* @param placeholderReplacerFunction a function defining how a placeholder will be replaced. It must not return
* null, instead it should throw a specific exception if a placeholder cannot be replaced.
* @param unresolvedInputHandler exception handler providing a exception which is thrown when placeholders
* remain unresolved, e.g. when brackets have the wrong order.
* @return the replaced input, if the input contains placeholders; the (same) input object, if no placeholders were
* contained in the input.
* @throws IllegalStateException if {@code placeholderReplacerFunction} returns null
* @throws DittoRuntimeException the passed in {@code unresolvedInputHandler} will be used in order to throw the
* DittoRuntimeException which was defined by the caller
*/
public static String substitute(final String input,
final Function<String, Optional<String>> placeholderReplacerFunction,
final Function<String, DittoRuntimeException> unresolvedInputHandler) {
return substitute(input, placeholderReplacerFunction, unresolvedInputHandler, false);
}
/**
* Substitutes any placeholder contained in the input.
*
* @param input the input.
* @param placeholderReplacerFunction a function defining how a placeholder will be replaced. It must not return
* null, instead it should throw a specific exception if a placeholder cannot be replaced.
* @param unresolvedInputHandler exception handler providing a exception which is thrown when placeholders
* remain unresolved, e.g. when brackets have the wrong order.
* @param allowUnresolved if {@code false} this method throws an exception if there are any unresolved
* placeholders after applying the given placeholder
* @return the replaced input, if the input contains placeholders; the (same) input object, if no placeholders were
* contained in the input.
* @throws IllegalStateException if {@code placeholderReplacerFunction} returns null
* @throws DittoRuntimeException if {@code allowUnresolved} is set to {@code false}, the passed in
* {@code unresolvedInputHandler} will be used in order to throw the DittoRuntimeException which was defined by the
* caller
*/
public static String substitute(final String input,
final Function<String, Optional<String>> placeholderReplacerFunction,
final Function<String, DittoRuntimeException> unresolvedInputHandler,
final boolean allowUnresolved) {
requireNonNull(input);
requireNonNull(placeholderReplacerFunction);
requireNonNull(unresolvedInputHandler);
final String substitutedStandardPlaceholder = substituteStandardPlaceholder(input, placeholderReplacerFunction,
unresolvedInputHandler, allowUnresolved);
return substituteLegacyPlaceholder(substitutedStandardPlaceholder, placeholderReplacerFunction,
unresolvedInputHandler, allowUnresolved);
}
private static String substituteLegacyPlaceholder(final String input,
final Function<String, Optional<String>> placeholderReplacerFunction,
final Function<String, DittoRuntimeException> unresolvedInputHandler, final boolean allowUnresolved) {
if (containsLegacyPlaceholder(input)) {
// for legacy placeholder we only allow request.subjectId, all other placeholders are unresolved
if (containsLegacyRequestSubjectIdPlaceholder(input)) {
final String substituted =
substitute(input, LEGACY_REQUEST_SUBJECT_ID_PATTERN, placeholderReplacerFunction);
if (!allowUnresolved && containsLegacyPlaceholder(substituted)) {
throw unresolvedInputHandler.apply(substituted);
} else {
return substituted;
}
} else {
throw unresolvedInputHandler.apply(input);
}
}
return input;
}
private static String substituteStandardPlaceholder(final String input,
final Function<String, Optional<String>> placeholderReplacerFunction,
final Function<String, DittoRuntimeException> unresolvedInputHandler, final boolean allowUnresolved) {
if (containsPlaceholder(input)) {
final String substituted = substitute(input, PLACEHOLDER_PATTERN, placeholderReplacerFunction);
if (!allowUnresolved && containsPlaceholder(substituted)) {
throw unresolvedInputHandler.apply(substituted);
}
return substituted;
} else {
return input;
}
}
private static String substitute(final String input, final Pattern pattern,
final Function<String, Optional<String>> replacerFunction) {
final Matcher matcher = pattern.matcher(input);
// replace with StringBuilder with JDK9
final AtomicReference<StringBuffer> bufferReference = new AtomicReference<>();
while (matcher.find()) {
final String placeholder = matcher.group(PLACEHOLDER_GROUP_NAME).trim();
replacerFunction.apply(placeholder)
.map(Matcher::quoteReplacement)
.ifPresent(replacement -> matcher.appendReplacement(lazyGet(bufferReference, StringBuffer::new),
replacement));
}
if (bufferReference.get() == null) { // no match -> return original string
return input;
} else { // there was at least one match -> append tail of original string
matcher.appendTail(bufferReference.get());
return bufferReference.get().toString();
}
}
/**
* Lazily initializes the given reference using the given initializer.
*
* @param reference the reference to the actual instance of type T
* @param initializer a supplier that initializes a new instance of T
* @param <T> the type of the instance
* @return the instance of type T, that is initialized on first access
*/
private static <T> T lazyGet(final AtomicReference<T> reference, final Supplier<T> initializer) {
T result = reference.get();
if (result == null) {
result = initializer.get();
if (!reference.compareAndSet(null, result)) {
return reference.get();
}
}
return result;
}
}