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         // prefixed model reflection
183         for (String prefix : getProjectPrefixes(request)) {
184             if (expression.startsWith(prefix)) {
185                 String subExpr = expression.substring(prefix.length());
186                 String v = projectProperty(model, projectDir, subExpr, true);
187                 if (v != null) {
188                     return v;
189                 }
190             }
191         }
192         // user properties
193         String value = request.getUserProperties().get(expression);
194         // model properties
195         if (value == null) {
196             value = model.getProperties().get(expression);
197         }
198         // system properties
199         if (value == null) {
200             value = request.getSystemProperties().get(expression);
201         }
202         // environment variables
203         if (value == null) {
204             value = request.getSystemProperties().get("env." + expression);
205         }
206         // un-prefixed model reflection
207         if (value == null) {
208             value = projectProperty(model, projectDir, expression, false);
209         }
210         return value;
211     }
212 
213     String projectProperty(Model model, Path projectDir, String subExpr, boolean prefixed) {
214         if (projectDir != null) {
215             if (subExpr.equals("basedir")) {
216                 return projectDir.toAbsolutePath().toString();
217             } else if (subExpr.startsWith("basedir.")) {
218                 try {
219                     Object value = ReflectionValueExtractor.evaluate(subExpr, projectDir.toAbsolutePath(), true);
220                     if (value != null) {
221                         return value.toString();
222                     }
223                 } catch (Exception e) {
224                     // addFeedback("Failed to extract \'" + expression + "\' from: " + root, e);
225                 }
226             } else if (prefixed && subExpr.equals("baseUri")) {
227                 return projectDir.toAbsolutePath().toUri().toASCIIString();
228             } else if (prefixed && subExpr.startsWith("baseUri.")) {
229                 try {
230                     Object value = ReflectionValueExtractor.evaluate(
231                             subExpr, projectDir.toAbsolutePath().toUri(), true);
232                     if (value != null) {
233                         return value.toString();
234                     }
235                 } catch (Exception e) {
236                     // addFeedback("Failed to extract \'" + expression + "\' from: " + root, e);
237                 }
238             } else if (prefixed && subExpr.equals("rootDirectory")) {
239                 return rootLocator.findMandatoryRoot(projectDir).toString();
240             } else if (prefixed && subExpr.startsWith("rootDirectory.")) {
241                 try {
242                     Object value =
243                             ReflectionValueExtractor.evaluate(subExpr, rootLocator.findMandatoryRoot(projectDir), true);
244                     if (value != null) {
245                         return value.toString();
246                     }
247                 } catch (Exception e) {
248                     // addFeedback("Failed to extract \'" + expression + "\' from: " + root, e);
249                 }
250             }
251         }
252         try {
253             Object value = ReflectionValueExtractor.evaluate(subExpr, model, false);
254             if (value != null) {
255                 return value.toString();
256             }
257         } catch (Exception e) {
258             // addFeedback("Failed to extract \'" + expression + "\' from: " + root, e);
259         }
260         return null;
261     }
262 }