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.project.interpolation;
20  
21  import javax.inject.Inject;
22  import javax.xml.stream.XMLStreamException;
23  
24  import java.io.File;
25  import java.io.IOException;
26  import java.io.StringReader;
27  import java.io.StringWriter;
28  import java.util.ArrayList;
29  import java.util.Arrays;
30  import java.util.Collections;
31  import java.util.List;
32  import java.util.Map;
33  import java.util.Properties;
34  
35  import org.apache.maven.model.Model;
36  import org.apache.maven.model.v4.MavenStaxReader;
37  import org.apache.maven.model.v4.MavenStaxWriter;
38  import org.apache.maven.project.DefaultProjectBuilderConfiguration;
39  import org.apache.maven.project.ProjectBuilderConfiguration;
40  import org.apache.maven.project.path.PathTranslator;
41  import org.codehaus.plexus.interpolation.AbstractValueSource;
42  import org.codehaus.plexus.interpolation.InterpolationException;
43  import org.codehaus.plexus.interpolation.InterpolationPostProcessor;
44  import org.codehaus.plexus.interpolation.Interpolator;
45  import org.codehaus.plexus.interpolation.MapBasedValueSource;
46  import org.codehaus.plexus.interpolation.ObjectBasedValueSource;
47  import org.codehaus.plexus.interpolation.PrefixAwareRecursionInterceptor;
48  import org.codehaus.plexus.interpolation.PrefixedObjectValueSource;
49  import org.codehaus.plexus.interpolation.PrefixedValueSourceWrapper;
50  import org.codehaus.plexus.interpolation.RecursionInterceptor;
51  import org.codehaus.plexus.interpolation.ValueSource;
52  import org.codehaus.plexus.logging.AbstractLogEnabled;
53  import org.codehaus.plexus.logging.Logger;
54  import org.codehaus.plexus.personality.plexus.lifecycle.phase.Initializable;
55  import org.codehaus.plexus.personality.plexus.lifecycle.phase.InitializationException;
56  
57  /**
58   * Use a regular expression search to find and resolve expressions within the POM.
59   *
60   * TODO Consolidate this logic with the PluginParameterExpressionEvaluator, minus deprecations/bans.
61   */
62  @Deprecated
63  public abstract class AbstractStringBasedModelInterpolator extends AbstractLogEnabled
64          implements ModelInterpolator, Initializable {
65  
66      private static final List<String> PROJECT_PREFIXES = Arrays.asList("pom.", "project.");
67  
68      private static final List<String> TRANSLATED_PATH_EXPRESSIONS;
69  
70      static {
71          List<String> translatedPrefixes = new ArrayList<>();
72  
73          // MNG-1927, MNG-2124, MNG-3355:
74          // If the build section is present and the project directory is non-null, we should make
75          // sure interpolation of the directories below uses translated paths.
76          // Afterward, we'll double back and translate any paths that weren't covered during interpolation via the
77          // code below...
78          translatedPrefixes.add("build.directory");
79          translatedPrefixes.add("build.outputDirectory");
80          translatedPrefixes.add("build.testOutputDirectory");
81          translatedPrefixes.add("build.sourceDirectory");
82          translatedPrefixes.add("build.testSourceDirectory");
83          translatedPrefixes.add("build.scriptSourceDirectory");
84          translatedPrefixes.add("reporting.outputDirectory");
85  
86          TRANSLATED_PATH_EXPRESSIONS = translatedPrefixes;
87      }
88  
89      @Inject
90      private PathTranslator pathTranslator;
91  
92      private Interpolator interpolator;
93  
94      private RecursionInterceptor recursionInterceptor;
95  
96      // for testing.
97      protected AbstractStringBasedModelInterpolator(PathTranslator pathTranslator) {
98          this.pathTranslator = pathTranslator;
99      }
100 
101     protected AbstractStringBasedModelInterpolator() {}
102 
103     @Override
104     public Model interpolate(Model model, Map<String, ?> context) throws ModelInterpolationException {
105         return interpolate(model, context, true);
106     }
107 
108     /**
109      * Serialize the inbound Model instance to a StringWriter, perform the regex replacement to resolve
110      * POM expressions, then re-parse into the resolved Model instance.
111      * <p>
112      * <b>NOTE:</b> This will result in a different instance of Model being returned!!!
113      *
114      * @param model The inbound Model instance, to serialize and reference for expression resolution
115      * @param context The other context map to be used during resolution
116      *
117      * @return The resolved instance of the inbound Model. This is a different instance!
118      *
119      * @deprecated Use {@link ModelInterpolator#interpolate(Model, File, ProjectBuilderConfiguration, boolean)} instead.
120      */
121     @Deprecated
122     @Override
123     public Model interpolate(Model model, Map<String, ?> context, boolean strict) throws ModelInterpolationException {
124         Properties props = new Properties();
125         props.putAll(context);
126 
127         return interpolate(model, null, new DefaultProjectBuilderConfiguration().setExecutionProperties(props), true);
128     }
129 
130     @Override
131     public Model interpolate(Model model, File projectDir, ProjectBuilderConfiguration config, boolean debugEnabled)
132             throws ModelInterpolationException {
133         StringWriter sWriter = new StringWriter(1024);
134 
135         MavenStaxWriter writer = new MavenStaxWriter();
136         try {
137             writer.write(sWriter, model.getDelegate());
138         } catch (IOException | XMLStreamException e) {
139             throw new ModelInterpolationException("Cannot serialize project model for interpolation.", e);
140         }
141 
142         String serializedModel = sWriter.toString();
143         serializedModel = interpolate(serializedModel, model, projectDir, config, debugEnabled);
144 
145         StringReader sReader = new StringReader(serializedModel);
146 
147         MavenStaxReader modelReader = new MavenStaxReader();
148         try {
149             model = new Model(modelReader.read(sReader));
150         } catch (XMLStreamException e) {
151             throw new ModelInterpolationException(
152                     "Cannot read project model from interpolating filter of serialized version.", e);
153         }
154 
155         return model;
156     }
157 
158     /**
159      * Interpolates all expressions in the src parameter.
160      * <p>
161      * The algorithm used for each expression is:
162      * <ul>
163      *   <li>If it starts with either "pom." or "project.", the expression is evaluated against the model.</li>
164      *   <li>If the value is null, get the value from the context.</li>
165      *   <li>If the value is null, but the context contains the expression, don't replace the expression string
166      *       with the value, and continue to find other expressions.</li>
167      *   <li>If the value is null, get it from the model properties.</li>
168      * </ul>
169      */
170     @Override
171     public String interpolate(
172             String src, Model model, final File projectDir, ProjectBuilderConfiguration config, boolean debug)
173             throws ModelInterpolationException {
174         try {
175             List<ValueSource> valueSources = createValueSources(model, projectDir, config);
176             List<InterpolationPostProcessor> postProcessors = createPostProcessors(model, projectDir, config);
177 
178             return interpolateInternal(src, valueSources, postProcessors, debug);
179         } finally {
180             interpolator.clearAnswers();
181         }
182     }
183 
184     protected List<ValueSource> createValueSources(
185             final Model model, final File projectDir, final ProjectBuilderConfiguration config) {
186         String timestampFormat = DEFAULT_BUILD_TIMESTAMP_FORMAT;
187 
188         Properties modelProperties = model.getProperties();
189         if (modelProperties != null) {
190             timestampFormat = modelProperties.getProperty(BUILD_TIMESTAMP_FORMAT_PROPERTY, timestampFormat);
191         }
192 
193         ValueSource modelValueSource1 = new PrefixedObjectValueSource(PROJECT_PREFIXES, model, false);
194         ValueSource modelValueSource2 = new ObjectBasedValueSource(model);
195 
196         ValueSource basedirValueSource = new PrefixedValueSourceWrapper(
197                 new AbstractValueSource(false) {
198 
199                     @Override
200                     public Object getValue(String expression) {
201                         if (projectDir != null && "basedir".equals(expression)) {
202                             return projectDir.getAbsolutePath();
203                         }
204                         return null;
205                     }
206                 },
207                 PROJECT_PREFIXES,
208                 true);
209         ValueSource baseUriValueSource = new PrefixedValueSourceWrapper(
210                 new AbstractValueSource(false) {
211 
212                     @Override
213                     public Object getValue(String expression) {
214                         if (projectDir != null && "baseUri".equals(expression)) {
215                             return projectDir.getAbsoluteFile().toPath().toUri().toASCIIString();
216                         }
217                         return null;
218                     }
219                 },
220                 PROJECT_PREFIXES,
221                 false);
222 
223         List<ValueSource> valueSources = new ArrayList<>(9);
224 
225         // NOTE: Order counts here!
226         valueSources.add(basedirValueSource);
227         valueSources.add(baseUriValueSource);
228         valueSources.add(new BuildTimestampValueSource(config.getBuildStartTime(), timestampFormat));
229         valueSources.add(modelValueSource1);
230         valueSources.add(new MapBasedValueSource(config.getUserProperties()));
231         valueSources.add(new MapBasedValueSource(modelProperties));
232         valueSources.add(new MapBasedValueSource(config.getExecutionProperties()));
233         valueSources.add(new AbstractValueSource(false) {
234 
235             @Override
236             public Object getValue(String expression) {
237                 return config.getExecutionProperties().getProperty("env." + expression);
238             }
239         });
240         valueSources.add(modelValueSource2);
241 
242         return valueSources;
243     }
244 
245     protected List<InterpolationPostProcessor> createPostProcessors(
246             final Model model, final File projectDir, final ProjectBuilderConfiguration config) {
247         return Collections.singletonList(new PathTranslatingPostProcessor(
248                 PROJECT_PREFIXES, TRANSLATED_PATH_EXPRESSIONS, projectDir, pathTranslator));
249     }
250 
251     @SuppressWarnings("unchecked")
252     protected String interpolateInternal(
253             String src, List<ValueSource> valueSources, List<InterpolationPostProcessor> postProcessors, boolean debug)
254             throws ModelInterpolationException {
255         if (!src.contains("${")) {
256             return src;
257         }
258 
259         Logger logger = getLogger();
260 
261         String result = src;
262         synchronized (this) {
263             for (ValueSource vs : valueSources) {
264                 interpolator.addValueSource(vs);
265             }
266 
267             for (InterpolationPostProcessor postProcessor : postProcessors) {
268                 interpolator.addPostProcessor(postProcessor);
269             }
270 
271             try {
272                 try {
273                     result = interpolator.interpolate(result, recursionInterceptor);
274                 } catch (InterpolationException e) {
275                     throw new ModelInterpolationException(e.getMessage(), e);
276                 }
277 
278                 if (debug) {
279                     List<Object> feedback = interpolator.getFeedback();
280                     if (feedback != null && !feedback.isEmpty()) {
281                         logger.debug("Maven encountered the following problems during initial POM interpolation:");
282 
283                         Object last = null;
284                         for (Object next : feedback) {
285                             if (next instanceof Throwable throwable) {
286                                 if (last == null) {
287                                     logger.debug("", throwable);
288                                 } else {
289                                     logger.debug(String.valueOf(last), throwable);
290                                 }
291                             } else {
292                                 if (last != null) {
293                                     logger.debug(String.valueOf(last));
294                                 }
295 
296                                 last = next;
297                             }
298                         }
299 
300                         if (last != null) {
301                             logger.debug(String.valueOf(last));
302                         }
303                     }
304                 }
305 
306                 interpolator.clearFeedback();
307             } finally {
308                 for (ValueSource vs : valueSources) {
309                     interpolator.removeValuesSource(vs);
310                 }
311 
312                 for (InterpolationPostProcessor postProcessor : postProcessors) {
313                     interpolator.removePostProcessor(postProcessor);
314                 }
315             }
316         }
317 
318         return result;
319     }
320 
321     protected RecursionInterceptor getRecursionInterceptor() {
322         return recursionInterceptor;
323     }
324 
325     protected void setRecursionInterceptor(RecursionInterceptor recursionInterceptor) {
326         this.recursionInterceptor = recursionInterceptor;
327     }
328 
329     protected abstract Interpolator createInterpolator();
330 
331     @Override
332     public void initialize() throws InitializationException {
333         interpolator = createInterpolator();
334         recursionInterceptor = new PrefixAwareRecursionInterceptor(PROJECT_PREFIXES);
335     }
336 
337     protected final Interpolator getInterpolator() {
338         return interpolator;
339     }
340 }