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