View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
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          // MNG-1927, MNG-2124, MNG-3355:
73          // If the build section is present and the project directory is non-null, we should make
74          // sure interpolation of the directories below uses translated paths.
75          // Afterward, we'll double back and translate any paths that weren't covered during interpolation via the
76          // code below...
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         // NOTE: Order counts here!
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      * Ensures that expressions referring to URLs evaluate to normalized URLs.
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      * Wraps an arbitrary object with an {@link ObjectBasedValueSource} instance, then
336      * wraps that source with a {@link PrefixedValueSourceWrapper} instance, to which
337      * this class delegates all of its calls.
338      */
339     public static class PrefixedObjectValueSource extends AbstractDelegatingValueSource
340             implements QueryEnabledValueSource {
341 
342         /**
343          * Wrap the specified root object, allowing the specified expression prefix.
344          * @param prefix the prefix.
345          * @param root the root of the graph.
346          */
347         public PrefixedObjectValueSource(String prefix, Object root) {
348             super(new PrefixedValueSourceWrapper(new ObjectBasedValueSource(root), prefix));
349         }
350 
351         /**
352          * Wrap the specified root object, allowing the specified list of expression
353          * prefixes and setting whether the {@link PrefixedValueSourceWrapper} allows
354          * unprefixed expressions.
355          * @param possiblePrefixes The possible prefixes.
356          * @param root The root of the graph.
357          * @param allowUnprefixedExpressions if we allow undefined expressions or not.
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          * {@inheritDoc}
367          */
368         public String getLastExpression() {
369             return ((QueryEnabledValueSource) getDelegate()).getLastExpression();
370         }
371     }
372 
373     /**
374      * Wraps an object, providing reflective access to the object graph of which the
375      * supplied object is the root. Expressions like 'child.name' will translate into
376      * 'rootObject.getChild().getName()' for non-boolean properties, and
377      * 'rootObject.getChild().isName()' for boolean properties.
378      */
379     public static class ObjectBasedValueSource extends AbstractValueSource {
380 
381         private final Object root;
382 
383         /**
384          * Construct a new value source, using the supplied object as the root from
385          * which to start, and using expressions split at the dot ('.') to navigate
386          * the object graph beneath this root.
387          * @param root the root of the graph.
388          */
389         public ObjectBasedValueSource(Object root) {
390             super(true);
391             this.root = root;
392         }
393 
394         /**
395          * <p>Split the expression into parts, tokenized on the dot ('.') character. Then,
396          * starting at the root object contained in this value source, apply each part
397          * to the object graph below this root, using either 'getXXX()' or 'isXXX()'
398          * accessor types to resolve the value for each successive expression part.
399          * Finally, return the result of the last expression part's resolution.</p>
400          *
401          * <p><b>NOTE:</b> The object-graph nagivation actually takes place via the
402          * {@link ReflectionValueExtractor} class.</p>
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      * Wraps another value source and intercepts interpolated expressions, checking for problems.
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 }