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.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          // MNG-1927, MNG-2124, MNG-3355:
76          // If the build section is present and the project directory is non-null, we should make
77          // sure interpolation of the directories below uses translated paths.
78          // Afterward, we'll double back and translate any paths that weren't covered during interpolation via the
79          // code below...
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         // NOTE: Order counts here!
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      * Ensures that expressions referring to URLs evaluate to normalized URLs.
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      * Wraps an arbitrary object with an {@link ObjectBasedValueSource} instance, then
339      * wraps that source with a {@link PrefixedValueSourceWrapper} instance, to which
340      * this class delegates all of its calls.
341      */
342     public static class PrefixedObjectValueSource extends AbstractDelegatingValueSource
343             implements QueryEnabledValueSource {
344 
345         /**
346          * Wrap the specified root object, allowing the specified expression prefix.
347          * @param prefix the prefix.
348          * @param root the root of the graph.
349          */
350         public PrefixedObjectValueSource(String prefix, Object root) {
351             super(new PrefixedValueSourceWrapper(new ObjectBasedValueSource(root), prefix));
352         }
353 
354         /**
355          * Wrap the specified root object, allowing the specified list of expression
356          * prefixes and setting whether the {@link PrefixedValueSourceWrapper} allows
357          * unprefixed expressions.
358          * @param possiblePrefixes The possible prefixes.
359          * @param root The root of the graph.
360          * @param allowUnprefixedExpressions if we allow undefined expressions or not.
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          * {@inheritDoc}
370          */
371         public String getLastExpression() {
372             return ((QueryEnabledValueSource) getDelegate()).getLastExpression();
373         }
374     }
375 
376     /**
377      * Wraps an object, providing reflective access to the object graph of which the
378      * supplied object is the root. Expressions like 'child.name' will translate into
379      * 'rootObject.getChild().getName()' for non-boolean properties, and
380      * 'rootObject.getChild().isName()' for boolean properties.
381      */
382     public static class ObjectBasedValueSource extends AbstractValueSource {
383 
384         private final Object root;
385 
386         /**
387          * Construct a new value source, using the supplied object as the root from
388          * which to start, and using expressions split at the dot ('.') to navigate
389          * the object graph beneath this root.
390          * @param root the root of the graph.
391          */
392         public ObjectBasedValueSource(Object root) {
393             super(true);
394             this.root = root;
395         }
396 
397         /**
398          * <p>Split the expression into parts, tokenized on the dot ('.') character. Then,
399          * starting at the root object contained in this value source, apply each part
400          * to the object graph below this root, using either 'getXXX()' or 'isXXX()'
401          * accessor types to resolve the value for each successive expression part.
402          * Finally, return the result of the last expression part's resolution.</p>
403          *
404          * <p><b>NOTE:</b> The object-graph nagivation actually takes place via the
405          * {@link ReflectionValueExtractor} class.</p>
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      * Wraps another value source and intercepts interpolated expressions, checking for problems.
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 }