-
Notifications
You must be signed in to change notification settings - Fork 214
/
ExpressionResolver.java
225 lines (200 loc) · 9.74 KB
/
ExpressionResolver.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
/*
* Copyright (c) 2021 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.placeholders;
import java.util.Collection;
import java.util.HashSet;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.ditto.base.model.common.Placeholders;
/**
* The ExpressionResolver is able to:
* <ul>
* <li>resolve {@link Placeholder}s in a passed {@code template} (based on {@link PlaceholderResolver}</li>
* <li>execute optional pipeline stages in a passed {@code template}</li>
* </ul>
* As a result, a resolved String is returned.
* For example, following expressions can be resolved:
* <ul>
* <li>{@code {{ thing:id }} }</li>
* <li>{@code {{ header:device_id }} }</li>
* <li>{@code {{ topic:full }} }</li>
* <li>{@code {{ thing:name | fn:substring-before(':') | fn:default(thing:name) }} }</li>
* <li>{@code {{ header:unknown | fn:default('fallback') }} }</li>
* </ul>
*/
public interface ExpressionResolver {
/**
* Resolve a single pipeline expression.
*
* @param pipelineExpression the pipeline expression.
* @return the pipeline element after evaluation.
* @throws UnresolvedPlaceholderException if not all placeholders were resolved
*/
PipelineElement resolveAsPipelineElement(String pipelineExpression);
/**
* Resolves a complete expression template starting with a {@link Placeholder} followed by optional pipeline stages
* (e.g. functions).
*
* @param expressionTemplate the expressionTemplate to resolve {@link Placeholder}s and and execute optional
* pipeline stages
* @return the resolved String, a signifier for resolution failure, or one for deletion.
* @throws PlaceholderFunctionTooComplexException thrown if the {@code expressionTemplate} contains a placeholder
* function chain which is too complex (e.g. too much chained function calls)
*/
default PipelineElement resolve(final String expressionTemplate) {
return ExpressionResolver.substitute(expressionTemplate, this::resolveAsPipelineElement);
}
/**
* Resolve a single pipeline expression.
*
* @param pipelineExpression the pipeline expression.
* @return the pipeline element after evaluation.
* @throws UnresolvedPlaceholderException if not all placeholders were resolved
*/
Stream<PipelineElement> resolveAsArrayPipelineElement(String pipelineExpression);
/**
* Resolves a complete expression template starting with a {@link Placeholder} followed by optional pipeline stages
* (e.g. functions).
*
* @param expressionTemplate the expressionTemplate to resolve {@link Placeholder}s and and execute optional
* pipeline stages
* @return the resolved String, a signifier for resolution failure, or one for deletion.
* @throws PlaceholderFunctionTooComplexException thrown if the {@code expressionTemplate} contains a placeholder
* function chain which is too complex (e.g. too much chained function calls)
*/
default Stream<PipelineElement> resolveAsArray(final String expressionTemplate) {
return ExpressionResolver.substituteArray(expressionTemplate, this::resolveAsArrayPipelineElement);
}
/**
* Resolves a complete expression template starting with a {@link Placeholder} followed by optional pipeline stages
* (e.g. functions). Keep unresolvable expressions as is.
*
* @param expressionTemplate the expressionTemplate to resolve {@link Placeholder}s and and execute optional
* pipeline stages
* @param forbiddenUnresolvedExpressionPrefixes a collection of expression prefixes which must be resolved
* @return the resolved String, a signifier for resolution failure, or one for deletion.
* @throws PlaceholderFunctionTooComplexException thrown if the {@code expressionTemplate} contains a placeholder
* function chain which is too complex (e.g. too much chained function calls)
* @throws UnresolvedPlaceholderException if placeholders could not be resolved which contained prefixed in the a
* provided {@code forbiddenUnresolvedExpressionPrefixes} list.
* @since 2.0.0
*/
default String resolvePartially(final String expressionTemplate,
final Collection<String> forbiddenUnresolvedExpressionPrefixes) {
return ExpressionResolver.substitute(expressionTemplate, expression -> {
final PipelineElement pipelineElement;
try {
pipelineElement = resolveAsPipelineElement(expression);
} catch (final UnresolvedPlaceholderException e) {
if (forbiddenUnresolvedExpressionPrefixes.stream().anyMatch(expression::startsWith)) {
throw e;
} else {
// placeholder is not supported; return the expression without resolution.
return PipelineElement.resolved("{{" + expression + "}}");
}
}
return pipelineElement.onUnresolved(() -> {
if (forbiddenUnresolvedExpressionPrefixes.stream().anyMatch(expression::startsWith)) {
throw UnresolvedPlaceholderException.newBuilder(expression).build();
}
return PipelineElement.resolved("{{" + expression + "}}");
});
}).toOptional().orElseThrow(() -> new IllegalStateException("Impossible"));
}
/**
* Perform simple substitution on a string based on a template function.
*
* @param input the input string.
* @param substitutionFunction the substitution function turning the content of each placeholder into a result.
* @return the substitution result.
*/
static PipelineElement substitute(
final String input,
final Function<String, PipelineElement> substitutionFunction) {
final Matcher matcher = Placeholders.pattern().matcher(input);
final StringBuffer resultBuilder = new StringBuffer();
while (matcher.find()) {
final String placeholderExpression = Placeholders.groupNames()
.stream()
.map(matcher::group)
.filter(Objects::nonNull)
.findAny()
.orElse("");
final PipelineElement element = substitutionFunction.apply(placeholderExpression);
switch (element.getType()) {
case DELETED:
case UNRESOLVED:
// abort pipeline execution: resolution failed or the string has been deleted.
return element;
default:
// proceed to append resolution result and evaluate the next pipeline expression
}
// append resolved placeholder
element.map(resolvedValue -> {
// increment counter inside matcher for "matcher.appendTail" later
matcher.appendReplacement(resultBuilder, "");
// actually append resolved value - do not attempt to interpret as regex
resultBuilder.append(resolvedValue);
return resolvedValue;
});
}
matcher.appendTail(resultBuilder);
return PipelineElement.resolved(resultBuilder.toString());
}
/**
* Perform simple substitution on a string based on a template function.
*
* @param input the input string.
* @param substitutionFunction the substitution function turning the content of each placeholder into a result.
* @return the substitution result.
*/
static Stream<PipelineElement> substituteArray(
final String input,
final Function<String, Stream<PipelineElement>> substitutionFunction) {
final Matcher matcher = Placeholders.pattern().matcher(input);
Collection<StringBuffer> resultBuilder = new HashSet<>();
resultBuilder.add(new StringBuffer());
while (matcher.find()) {
final String placeholderExpression = Placeholders.groupNames()
.stream()
.map(matcher::group)
.filter(Objects::nonNull)
.findAny()
.orElse("");
final Collection<StringBuffer> finalResultBuilder = resultBuilder;
final Stream<PipelineElement> elements = substitutionFunction.apply(placeholderExpression);
final AtomicInteger counter = new AtomicInteger();
resultBuilder = elements
.filter(element -> !element.getType().equals(PipelineElement.Type.UNRESOLVED))
.flatMap(element -> {
// append resolved placeholder
return finalResultBuilder.stream()
.peek(builder -> {
if (counter.get() == 0) {
counter.getAndIncrement();
matcher.appendReplacement(builder, "");
}
})
.map(string -> new StringBuffer(string).append(element.toOptional().get()));
}).collect(Collectors.toSet());
}
return resultBuilder.stream()
.map(matcher::appendTail)
.map(StringBuffer::toString)
.map(PipelineElement::resolved);
}
}