1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 package org.apache.maven.internal.impl.model;
20
21 import java.net.URI;
22 import java.nio.file.Path;
23 import java.time.Instant;
24 import java.util.ArrayList;
25 import java.util.Arrays;
26 import java.util.Collection;
27 import java.util.Collections;
28 import java.util.HashMap;
29 import java.util.HashSet;
30 import java.util.List;
31 import java.util.Map;
32 import java.util.Set;
33
34 import org.apache.maven.api.di.Inject;
35 import org.apache.maven.api.di.Named;
36 import org.apache.maven.api.di.Singleton;
37 import org.apache.maven.api.model.Model;
38 import org.apache.maven.api.services.BuilderProblem;
39 import org.apache.maven.api.services.ModelBuilderRequest;
40 import org.apache.maven.api.services.ModelProblem;
41 import org.apache.maven.api.services.ModelProblemCollector;
42 import org.apache.maven.api.services.model.ModelInterpolator;
43 import org.apache.maven.api.services.model.PathTranslator;
44 import org.apache.maven.api.services.model.RootLocator;
45 import org.apache.maven.api.services.model.UrlNormalizer;
46 import org.apache.maven.model.v4.MavenTransformer;
47 import org.codehaus.plexus.interpolation.AbstractDelegatingValueSource;
48 import org.codehaus.plexus.interpolation.AbstractValueSource;
49 import org.codehaus.plexus.interpolation.InterpolationException;
50 import org.codehaus.plexus.interpolation.InterpolationPostProcessor;
51 import org.codehaus.plexus.interpolation.MapBasedValueSource;
52 import org.codehaus.plexus.interpolation.PrefixAwareRecursionInterceptor;
53 import org.codehaus.plexus.interpolation.PrefixedValueSourceWrapper;
54 import org.codehaus.plexus.interpolation.QueryEnabledValueSource;
55 import org.codehaus.plexus.interpolation.RecursionInterceptor;
56 import org.codehaus.plexus.interpolation.StringSearchInterpolator;
57 import org.codehaus.plexus.interpolation.ValueSource;
58 import org.codehaus.plexus.interpolation.reflection.ReflectionValueExtractor;
59 import org.codehaus.plexus.interpolation.util.ValueSourceUtils;
60
61 @Named
62 @Singleton
63 public class DefaultModelInterpolator implements ModelInterpolator {
64
65 private static final String PREFIX_PROJECT = "project.";
66 private static final String PREFIX_POM = "pom.";
67 private static final List<String> PROJECT_PREFIXES_3_1 = Arrays.asList(PREFIX_POM, PREFIX_PROJECT);
68 private static final List<String> PROJECT_PREFIXES_4_0 = Collections.singletonList(PREFIX_PROJECT);
69
70 private static final Collection<String> TRANSLATED_PATH_EXPRESSIONS;
71
72 static {
73 Collection<String> translatedPrefixes = new HashSet<>();
74
75
76
77
78
79
80 translatedPrefixes.add("build.directory");
81 translatedPrefixes.add("build.outputDirectory");
82 translatedPrefixes.add("build.testOutputDirectory");
83 translatedPrefixes.add("build.sourceDirectory");
84 translatedPrefixes.add("build.testSourceDirectory");
85 translatedPrefixes.add("build.scriptSourceDirectory");
86 translatedPrefixes.add("reporting.outputDirectory");
87
88 TRANSLATED_PATH_EXPRESSIONS = translatedPrefixes;
89 }
90
91 private final PathTranslator pathTranslator;
92 private final UrlNormalizer urlNormalizer;
93 private final RootLocator rootLocator;
94
95 @Inject
96 public DefaultModelInterpolator(
97 PathTranslator pathTranslator, UrlNormalizer urlNormalizer, RootLocator rootLocator) {
98 this.pathTranslator = pathTranslator;
99 this.urlNormalizer = urlNormalizer;
100 this.rootLocator = rootLocator;
101 }
102
103 interface InnerInterpolator {
104 String interpolate(String value);
105 }
106
107 @Override
108 public Model interpolateModel(
109 Model model, Path projectDir, ModelBuilderRequest request, ModelProblemCollector problems) {
110 List<? extends ValueSource> valueSources = createValueSources(model, projectDir, request, problems);
111 List<? extends InterpolationPostProcessor> postProcessors = createPostProcessors(model, projectDir, request);
112
113 InnerInterpolator innerInterpolator = createInterpolator(valueSources, postProcessors, request, problems);
114
115 return new MavenTransformer(innerInterpolator::interpolate).visit(model);
116 }
117
118 private InnerInterpolator createInterpolator(
119 List<? extends ValueSource> valueSources,
120 List<? extends InterpolationPostProcessor> postProcessors,
121 ModelBuilderRequest request,
122 ModelProblemCollector problems) {
123 Map<String, String> cache = new HashMap<>();
124 StringSearchInterpolator interpolator = new StringSearchInterpolator();
125 interpolator.setCacheAnswers(true);
126 for (ValueSource vs : valueSources) {
127 interpolator.addValueSource(vs);
128 }
129 for (InterpolationPostProcessor postProcessor : postProcessors) {
130 interpolator.addPostProcessor(postProcessor);
131 }
132 RecursionInterceptor recursionInterceptor = createRecursionInterceptor(request);
133 return value -> {
134 if (value != null && value.contains("${")) {
135 String c = cache.get(value);
136 if (c == null) {
137 try {
138 c = interpolator.interpolate(value, recursionInterceptor);
139 } catch (InterpolationException e) {
140 problems.add(BuilderProblem.Severity.ERROR, ModelProblem.Version.BASE, e.getMessage(), e);
141 }
142 cache.put(value, c);
143 }
144 return c;
145 }
146 return value;
147 };
148 }
149
150 protected List<String> getProjectPrefixes(ModelBuilderRequest request) {
151 return request.getValidationLevel() >= ModelBuilderRequest.VALIDATION_LEVEL_MAVEN_4_0
152 ? PROJECT_PREFIXES_4_0
153 : PROJECT_PREFIXES_3_1;
154 }
155
156 protected List<ValueSource> createValueSources(
157 Model model, Path projectDir, ModelBuilderRequest request, ModelProblemCollector problems) {
158 Map<String, String> modelProperties = model.getProperties();
159
160 ValueSource projectPrefixValueSource;
161 ValueSource prefixlessObjectBasedValueSource;
162 if (request.getValidationLevel() >= ModelBuilderRequest.VALIDATION_LEVEL_MAVEN_4_0) {
163 projectPrefixValueSource = new PrefixedObjectValueSource(PROJECT_PREFIXES_4_0, model, false);
164 prefixlessObjectBasedValueSource = new ObjectBasedValueSource(model);
165 } else {
166 projectPrefixValueSource = new PrefixedObjectValueSource(PROJECT_PREFIXES_3_1, model, false);
167 if (request.getValidationLevel() >= ModelBuilderRequest.VALIDATION_LEVEL_MAVEN_2_0) {
168 projectPrefixValueSource =
169 new ProblemDetectingValueSource(projectPrefixValueSource, PREFIX_POM, PREFIX_PROJECT, problems);
170 }
171
172 prefixlessObjectBasedValueSource = new ObjectBasedValueSource(model);
173 if (request.getValidationLevel() >= ModelBuilderRequest.VALIDATION_LEVEL_MAVEN_2_0) {
174 prefixlessObjectBasedValueSource =
175 new ProblemDetectingValueSource(prefixlessObjectBasedValueSource, "", PREFIX_PROJECT, problems);
176 }
177 }
178
179
180 List<ValueSource> valueSources = new ArrayList<>(9);
181
182 if (projectDir != null) {
183 ValueSource basedirValueSource = new PrefixedValueSourceWrapper(
184 new AbstractValueSource(false) {
185 @Override
186 public Object getValue(String expression) {
187 if ("basedir".equals(expression)) {
188 return projectDir.toAbsolutePath().toString();
189 } else if (expression.startsWith("basedir.")) {
190 Path basedir = projectDir.toAbsolutePath();
191 return new ObjectBasedValueSource(basedir)
192 .getValue(expression.substring("basedir.".length()));
193 }
194 return null;
195 }
196 },
197 getProjectPrefixes(request),
198 true);
199 valueSources.add(basedirValueSource);
200
201 ValueSource baseUriValueSource = new PrefixedValueSourceWrapper(
202 new AbstractValueSource(false) {
203 @Override
204 public Object getValue(String expression) {
205 if ("baseUri".equals(expression)) {
206 return projectDir.toAbsolutePath().toUri().toASCIIString();
207 } else if (expression.startsWith("baseUri.")) {
208 URI baseUri = projectDir.toAbsolutePath().toUri();
209 return new ObjectBasedValueSource(baseUri)
210 .getValue(expression.substring("baseUri.".length()));
211 }
212 return null;
213 }
214 },
215 getProjectPrefixes(request),
216 false);
217 valueSources.add(baseUriValueSource);
218 valueSources.add(new BuildTimestampValueSource(request.getSession().getStartTime(), modelProperties));
219 }
220
221 valueSources.add(new PrefixedValueSourceWrapper(
222 new AbstractValueSource(false) {
223 @Override
224 public Object getValue(String expression) {
225 if ("rootDirectory".equals(expression)) {
226 Path root = rootLocator.findMandatoryRoot(projectDir);
227 return root.toFile().getPath();
228 } else if (expression.startsWith("rootDirectory.")) {
229 Path root = rootLocator.findMandatoryRoot(projectDir);
230 return new ObjectBasedValueSource(root)
231 .getValue(expression.substring("rootDirectory.".length()));
232 }
233 return null;
234 }
235 },
236 getProjectPrefixes(request)));
237
238 valueSources.add(projectPrefixValueSource);
239
240 valueSources.add(new MapBasedValueSource(request.getUserProperties()));
241
242 valueSources.add(new MapBasedValueSource(modelProperties));
243
244 valueSources.add(new MapBasedValueSource(request.getSystemProperties()));
245
246 valueSources.add(new AbstractValueSource(false) {
247 @Override
248 public Object getValue(String expression) {
249 return request.getSystemProperties().get("env." + expression);
250 }
251 });
252
253 valueSources.add(prefixlessObjectBasedValueSource);
254
255 return valueSources;
256 }
257
258 protected List<? extends InterpolationPostProcessor> createPostProcessors(
259 Model model, Path projectDir, ModelBuilderRequest request) {
260 List<InterpolationPostProcessor> processors = new ArrayList<>(2);
261 if (projectDir != null) {
262 processors.add(new PathTranslatingPostProcessor(
263 getProjectPrefixes(request), TRANSLATED_PATH_EXPRESSIONS, projectDir, pathTranslator));
264 }
265 processors.add(new UrlNormalizingPostProcessor(urlNormalizer));
266 return processors;
267 }
268
269 protected RecursionInterceptor createRecursionInterceptor(ModelBuilderRequest request) {
270 return new PrefixAwareRecursionInterceptor(getProjectPrefixes(request));
271 }
272
273 static class PathTranslatingPostProcessor implements InterpolationPostProcessor {
274
275 private final Collection<String> unprefixedPathKeys;
276 private final Path projectDir;
277 private final PathTranslator pathTranslator;
278 private final List<String> expressionPrefixes;
279
280 PathTranslatingPostProcessor(
281 List<String> expressionPrefixes,
282 Collection<String> unprefixedPathKeys,
283 Path projectDir,
284 PathTranslator pathTranslator) {
285 this.expressionPrefixes = expressionPrefixes;
286 this.unprefixedPathKeys = unprefixedPathKeys;
287 this.projectDir = projectDir;
288 this.pathTranslator = pathTranslator;
289 }
290
291 @Override
292 public Object execute(String expression, Object value) {
293 if (value != null) {
294 expression = ValueSourceUtils.trimPrefix(expression, expressionPrefixes, true);
295 if (unprefixedPathKeys.contains(expression)) {
296 return pathTranslator.alignToBaseDirectory(String.valueOf(value), projectDir);
297 }
298 }
299 return null;
300 }
301 }
302
303
304
305
306
307 static class UrlNormalizingPostProcessor implements InterpolationPostProcessor {
308
309 private static final Set<String> URL_EXPRESSIONS;
310
311 static {
312 Set<String> expressions = new HashSet<>();
313 expressions.add("project.url");
314 expressions.add("project.scm.url");
315 expressions.add("project.scm.connection");
316 expressions.add("project.scm.developerConnection");
317 expressions.add("project.distributionManagement.site.url");
318 URL_EXPRESSIONS = expressions;
319 }
320
321 private final UrlNormalizer normalizer;
322
323 UrlNormalizingPostProcessor(UrlNormalizer normalizer) {
324 this.normalizer = normalizer;
325 }
326
327 @Override
328 public Object execute(String expression, Object value) {
329 if (value != null && URL_EXPRESSIONS.contains(expression)) {
330 return normalizer.normalize(value.toString());
331 }
332
333 return null;
334 }
335 }
336
337
338
339
340
341
342 public static class PrefixedObjectValueSource extends AbstractDelegatingValueSource
343 implements QueryEnabledValueSource {
344
345
346
347
348
349
350 public PrefixedObjectValueSource(String prefix, Object root) {
351 super(new PrefixedValueSourceWrapper(new ObjectBasedValueSource(root), prefix));
352 }
353
354
355
356
357
358
359
360
361
362 public PrefixedObjectValueSource(
363 List<String> possiblePrefixes, Object root, boolean allowUnprefixedExpressions) {
364 super(new PrefixedValueSourceWrapper(
365 new ObjectBasedValueSource(root), possiblePrefixes, allowUnprefixedExpressions));
366 }
367
368
369
370
371 public String getLastExpression() {
372 return ((QueryEnabledValueSource) getDelegate()).getLastExpression();
373 }
374 }
375
376
377
378
379
380
381
382 public static class ObjectBasedValueSource extends AbstractValueSource {
383
384 private final Object root;
385
386
387
388
389
390
391
392 public ObjectBasedValueSource(Object root) {
393 super(true);
394 this.root = root;
395 }
396
397
398
399
400
401
402
403
404
405
406
407 public Object getValue(String expression) {
408 if (expression == null || expression.trim().isEmpty()) {
409 return null;
410 }
411
412 try {
413 return ReflectionValueExtractor.evaluate(expression, root, false);
414 } catch (Exception e) {
415 addFeedback("Failed to extract \'" + expression + "\' from: " + root, e);
416 }
417
418 return null;
419 }
420 }
421
422
423
424
425
426 static class ProblemDetectingValueSource implements ValueSource {
427
428 private final ValueSource valueSource;
429
430 private final String bannedPrefix;
431
432 private final String newPrefix;
433
434 private final ModelProblemCollector problems;
435
436 ProblemDetectingValueSource(
437 ValueSource valueSource, String bannedPrefix, String newPrefix, ModelProblemCollector problems) {
438 this.valueSource = valueSource;
439 this.bannedPrefix = bannedPrefix;
440 this.newPrefix = newPrefix;
441 this.problems = problems;
442 }
443
444 @Override
445 public Object getValue(String expression) {
446 Object value = valueSource.getValue(expression);
447
448 if (value != null && expression.startsWith(bannedPrefix)) {
449 String msg = "The expression ${" + expression + "} is deprecated.";
450 if (newPrefix != null && !newPrefix.isEmpty()) {
451 msg += " Please use ${" + newPrefix + expression.substring(bannedPrefix.length()) + "} instead.";
452 }
453 problems.add(BuilderProblem.Severity.WARNING, ModelProblem.Version.V20, msg);
454 }
455
456 return value;
457 }
458
459 @Override
460 public List getFeedback() {
461 return valueSource.getFeedback();
462 }
463
464 @Override
465 public void clearFeedback() {
466 valueSource.clearFeedback();
467 }
468 }
469
470 static class BuildTimestampValueSource extends AbstractValueSource {
471 private final Instant startTime;
472 private final Map<String, String> properties;
473
474 BuildTimestampValueSource(Instant startTime, Map<String, String> properties) {
475 super(false);
476 this.startTime = startTime;
477 this.properties = properties;
478 }
479
480 @Override
481 public Object getValue(String expression) {
482 if ("build.timestamp".equals(expression) || "maven.build.timestamp".equals(expression)) {
483 return new MavenBuildTimestamp(startTime, properties).formattedTimestamp();
484 }
485 return null;
486 }
487 }
488 }