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.impl.model;
20  
21  import java.nio.file.Path;
22  import java.util.Arrays;
23  import java.util.Collections;
24  import java.util.HashMap;
25  import java.util.List;
26  import java.util.Map;
27  import java.util.Optional;
28  import java.util.Set;
29  import java.util.function.BinaryOperator;
30  import java.util.function.Function;
31  import java.util.function.UnaryOperator;
32  
33  import org.apache.maven.api.di.Inject;
34  import org.apache.maven.api.di.Named;
35  import org.apache.maven.api.di.Singleton;
36  import org.apache.maven.api.model.Model;
37  import org.apache.maven.api.services.BuilderProblem;
38  import org.apache.maven.api.services.Interpolator;
39  import org.apache.maven.api.services.InterpolatorException;
40  import org.apache.maven.api.services.ModelBuilderRequest;
41  import org.apache.maven.api.services.ModelProblem;
42  import org.apache.maven.api.services.ModelProblemCollector;
43  import org.apache.maven.api.services.model.ModelInterpolator;
44  import org.apache.maven.api.services.model.PathTranslator;
45  import org.apache.maven.api.services.model.RootLocator;
46  import org.apache.maven.api.services.model.UrlNormalizer;
47  import org.apache.maven.impl.model.reflection.ReflectionValueExtractor;
48  import org.apache.maven.model.v4.MavenTransformer;
49  
50  @Named
51  @Singleton
52  public class DefaultModelInterpolator implements ModelInterpolator {
53  
54      private static final String PREFIX_PROJECT = "project.";
55      private static final String PREFIX_POM = "pom.";
56      private static final List<String> PROJECT_PREFIXES_3_1 = Arrays.asList(PREFIX_POM, PREFIX_PROJECT);
57      private static final List<String> PROJECT_PREFIXES_4_0 = Collections.singletonList(PREFIX_PROJECT);
58  
59      // MNG-1927, MNG-2124, MNG-3355:
60      // If the build section is present and the project directory is non-null, we should make
61      // sure interpolation of the directories below uses translated paths.
62      // Afterward, we'll double back and translate any paths that weren't covered during interpolation via the
63      // code below...
64      private static final Set<String> TRANSLATED_PATH_EXPRESSIONS = Set.of(
65              "build.directory",
66              "build.outputDirectory",
67              "build.testOutputDirectory",
68              "build.sourceDirectory",
69              "build.testSourceDirectory",
70              "build.scriptSourceDirectory",
71              "reporting.outputDirectory");
72  
73      private static final Set<String> URL_EXPRESSIONS = Set.of(
74              "project.url",
75              "project.scm.url",
76              "project.scm.connection",
77              "project.scm.developerConnection",
78              "project.distributionManagement.site.url");
79  
80      private final PathTranslator pathTranslator;
81      private final UrlNormalizer urlNormalizer;
82      private final RootLocator rootLocator;
83      private final Interpolator interpolator;
84  
85      @Inject
86      public DefaultModelInterpolator(
87              PathTranslator pathTranslator,
88              UrlNormalizer urlNormalizer,
89              RootLocator rootLocator,
90              Interpolator interpolator) {
91          this.pathTranslator = pathTranslator;
92          this.urlNormalizer = urlNormalizer;
93          this.rootLocator = rootLocator;
94          this.interpolator = interpolator;
95      }
96  
97      interface InnerInterpolator {
98          String interpolate(String value);
99      }
100 
101     @Override
102     public Model interpolateModel(
103             Model model, Path projectDir, ModelBuilderRequest request, ModelProblemCollector problems) {
104         InnerInterpolator innerInterpolator = createInterpolator(model, projectDir, request, problems);
105         return new MavenTransformer(innerInterpolator::interpolate).visit(model);
106     }
107 
108     private InnerInterpolator createInterpolator(
109             Model model, Path projectDir, ModelBuilderRequest request, ModelProblemCollector problems) {
110 
111         Map<String, Optional<String>> cache = new HashMap<>();
112         Function<String, Optional<String>> ucb =
113                 v -> Optional.ofNullable(callback(model, projectDir, request, problems, v));
114         UnaryOperator<String> cb = v -> cache.computeIfAbsent(v, ucb).orElse(null);
115         BinaryOperator<String> postprocessor = (e, v) -> postProcess(projectDir, request, e, v);
116         return value -> {
117             try {
118                 return interpolator.interpolate(value, cb, postprocessor, false);
119             } catch (InterpolatorException e) {
120                 problems.add(BuilderProblem.Severity.ERROR, ModelProblem.Version.BASE, e.getMessage(), e);
121                 return null;
122             }
123         };
124     }
125 
126     protected List<String> getProjectPrefixes(ModelBuilderRequest request) {
127         return request.getRequestType() == ModelBuilderRequest.RequestType.BUILD_PROJECT
128                 ? PROJECT_PREFIXES_4_0
129                 : PROJECT_PREFIXES_3_1;
130     }
131 
132     String callback(
133             Model model,
134             Path projectDir,
135             ModelBuilderRequest request,
136             ModelProblemCollector problems,
137             String expression) {
138         String value = doCallback(model, projectDir, request, problems, expression);
139         if (value != null) {
140             // value = postProcess(projectDir, request, expression, value);
141         }
142         return value;
143     }
144 
145     private String postProcess(Path projectDir, ModelBuilderRequest request, String expression, String value) {
146         // path translation
147         String exp = unprefix(expression, getProjectPrefixes(request));
148         if (TRANSLATED_PATH_EXPRESSIONS.contains(exp)) {
149             value = pathTranslator.alignToBaseDirectory(value, projectDir);
150         }
151         // normalize url
152         if (URL_EXPRESSIONS.contains(expression)) {
153             value = urlNormalizer.normalize(value);
154         }
155         return value;
156     }
157 
158     private String unprefix(String expression, List<String> prefixes) {
159         for (String prefix : prefixes) {
160             if (expression.startsWith(prefix)) {
161                 return expression.substring(prefix.length());
162             }
163         }
164         return expression;
165     }
166 
167     String doCallback(
168             Model model,
169             Path projectDir,
170             ModelBuilderRequest request,
171             ModelProblemCollector problems,
172             String expression) {
173         // basedir (the prefixed combos are handled below)
174         if ("basedir".equals(expression)) {
175             return projectProperty(model, projectDir, expression, false);
176         }
177         // timestamp
178         if ("build.timestamp".equals(expression) || "maven.build.timestamp".equals(expression)) {
179             return new MavenBuildTimestamp(request.getSession().getStartTime(), model.getProperties())
180                     .formattedTimestamp();
181         }
182         // user properties
183         String value = request.getUserProperties().get(expression);
184         // model properties (check before prefixed model reflection to avoid recursion)
185         if (value == null) {
186             value = model.getProperties().get(expression);
187         }
188         // prefixed model reflection
189         if (value == null) {
190             for (String prefix : getProjectPrefixes(request)) {
191                 if (expression.startsWith(prefix)) {
192                     String subExpr = expression.substring(prefix.length());
193                     value = projectProperty(model, projectDir, subExpr, true);
194                     if (value != null) {
195                         return value;
196                     }
197                 }
198             }
199         }
200         // system properties
201         if (value == null) {
202             value = request.getSystemProperties().get(expression);
203         }
204         // environment variables
205         if (value == null) {
206             value = request.getSystemProperties().get("env." + expression);
207         }
208         // un-prefixed model reflection
209         if (value == null) {
210             value = projectProperty(model, projectDir, expression, false);
211         }
212         return value;
213     }
214 
215     String projectProperty(Model model, Path projectDir, String subExpr, boolean prefixed) {
216         if (projectDir != null) {
217             if (subExpr.equals("basedir")) {
218                 return projectDir.toAbsolutePath().toString();
219             } else if (subExpr.startsWith("basedir.")) {
220                 try {
221                     Object value = ReflectionValueExtractor.evaluate(subExpr, projectDir.toAbsolutePath(), true);
222                     if (value != null) {
223                         return value.toString();
224                     }
225                 } catch (Exception e) {
226                     // addFeedback("Failed to extract \'" + expression + "\' from: " + root, e);
227                 }
228             } else if (prefixed && subExpr.equals("baseUri")) {
229                 return projectDir.toAbsolutePath().toUri().toASCIIString();
230             } else if (prefixed && subExpr.startsWith("baseUri.")) {
231                 try {
232                     Object value = ReflectionValueExtractor.evaluate(
233                             subExpr, projectDir.toAbsolutePath().toUri(), true);
234                     if (value != null) {
235                         return value.toString();
236                     }
237                 } catch (Exception e) {
238                     // addFeedback("Failed to extract \'" + expression + "\' from: " + root, e);
239                 }
240             } else if (prefixed && subExpr.equals("rootDirectory")) {
241                 return rootLocator.findMandatoryRoot(projectDir).toString();
242             } else if (prefixed && subExpr.startsWith("rootDirectory.")) {
243                 try {
244                     Object value =
245                             ReflectionValueExtractor.evaluate(subExpr, rootLocator.findMandatoryRoot(projectDir), true);
246                     if (value != null) {
247                         return value.toString();
248                     }
249                 } catch (Exception e) {
250                     // addFeedback("Failed to extract \'" + expression + "\' from: " + root, e);
251                 }
252             }
253         }
254         try {
255             Object value = ReflectionValueExtractor.evaluate(subExpr, model, false);
256             if (value != null) {
257                 return value.toString();
258             }
259         } catch (Exception e) {
260             // addFeedback("Failed to extract \'" + expression + "\' from: " + root, e);
261         }
262         return null;
263     }
264 }