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.io.File;
22  import java.io.IOException;
23  import java.nio.file.FileVisitResult;
24  import java.nio.file.Files;
25  import java.nio.file.Path;
26  import java.nio.file.PathMatcher;
27  import java.nio.file.Paths;
28  import java.nio.file.SimpleFileVisitor;
29  import java.nio.file.attribute.BasicFileAttributes;
30  import java.util.Collections;
31  import java.util.HashMap;
32  import java.util.List;
33  import java.util.Map;
34  import java.util.Objects;
35  import java.util.concurrent.atomic.AtomicBoolean;
36  
37  import org.apache.maven.api.model.Model;
38  import org.apache.maven.api.services.Interpolator;
39  import org.apache.maven.api.services.InterpolatorException;
40  import org.apache.maven.api.services.ModelBuilderException;
41  import org.apache.maven.api.services.ProjectBuilderException;
42  import org.apache.maven.api.services.model.PathTranslator;
43  import org.apache.maven.api.services.model.ProfileActivationContext;
44  import org.apache.maven.api.services.model.RootLocator;
45  
46  /**
47   * Describes the environmental context used to determine the activation status of profiles.
48   */
49  public class DefaultProfileActivationContext implements ProfileActivationContext {
50  
51      record ExistRequest(String path, boolean enableGlob) {}
52  
53      enum ModelInfo {
54          ArtifactId,
55          Packaging,
56          BaseDirectory,
57          RootDirectory
58      }
59  
60      /**
61       * This class keeps track of information that are used during profile activation.
62       * This allows to cache the activated parent and check if the result of the
63       * activation will be the same by verifying that the used keys are the same.
64       */
65      static class Record {
66          private final Map<String, Boolean> usedActiveProfiles = new HashMap<>();
67          private final Map<String, Boolean> usedInactiveProfiles = new HashMap<>();
68          private final Map<String, String> usedSystemProperties = new HashMap<>();
69          private final Map<String, String> usedUserProperties = new HashMap<>();
70          private final Map<String, String> usedModelProperties = new HashMap<>();
71          private final Map<ModelInfo, String> usedModelInfos = new HashMap<>();
72          private final Map<ExistRequest, Boolean> usedExists = new HashMap<>();
73  
74          @Override
75          public boolean equals(Object o) {
76              if (o instanceof Record record) {
77                  return Objects.equals(usedActiveProfiles, record.usedActiveProfiles)
78                          && Objects.equals(usedInactiveProfiles, record.usedInactiveProfiles)
79                          && Objects.equals(usedSystemProperties, record.usedSystemProperties)
80                          && Objects.equals(usedUserProperties, record.usedUserProperties)
81                          && Objects.equals(usedModelProperties, record.usedModelProperties)
82                          && Objects.equals(usedModelInfos, record.usedModelInfos)
83                          && Objects.equals(usedExists, record.usedExists);
84              }
85              return false;
86          }
87  
88          @Override
89          public int hashCode() {
90              return Objects.hash(
91                      usedActiveProfiles,
92                      usedInactiveProfiles,
93                      usedSystemProperties,
94                      usedUserProperties,
95                      usedModelProperties,
96                      usedModelInfos,
97                      usedExists);
98          }
99  
100         boolean matches(DefaultProfileActivationContext context) {
101             return matchesProfiles(usedActiveProfiles, context.activeProfileIds)
102                     && matchesProfiles(usedInactiveProfiles, context.inactiveProfileIds)
103                     && matchesProperties(usedSystemProperties, context.systemProperties)
104                     && matchesProperties(usedUserProperties, context.userProperties)
105                     && matchesProperties(usedModelProperties, context.model.getProperties())
106                     && matchesModelInfos(usedModelInfos, context)
107                     && matchesExists(usedExists, context);
108         }
109 
110         private boolean matchesProfiles(Map<String, Boolean> expected, List<String> actual) {
111             return expected.entrySet().stream()
112                     .allMatch(e -> Objects.equals(e.getValue(), actual.contains(e.getKey())));
113         }
114 
115         private boolean matchesProperties(Map<String, String> expected, Map<String, String> actual) {
116             return expected.entrySet().stream().allMatch(e -> Objects.equals(e.getValue(), actual.get(e.getKey())));
117         }
118 
119         private boolean matchesModelInfos(Map<ModelInfo, String> infos, DefaultProfileActivationContext context) {
120             return infos.entrySet().stream()
121                     .allMatch(e -> Objects.equals(e.getValue(), getModelValue(e.getKey(), context)));
122         }
123 
124         private String getModelValue(ModelInfo key, DefaultProfileActivationContext context) {
125             return switch (key) {
126                 case ArtifactId -> context.model.getArtifactId();
127                 case Packaging -> context.model.getPackaging();
128                 case BaseDirectory -> context.doGetModelBaseDirectory();
129                 case RootDirectory -> context.doGetModelRootDirectory();
130             };
131         }
132 
133         private boolean matchesExists(Map<ExistRequest, Boolean> exists, DefaultProfileActivationContext context) {
134             return exists.entrySet().stream()
135                     .allMatch(e -> Objects.equals(
136                             e.getValue(),
137                             context.doExists(e.getKey().path(), e.getKey().enableGlob())));
138         }
139     }
140 
141     private final PathTranslator pathTranslator;
142     private final RootLocator rootLocator;
143     private final Interpolator interpolator;
144 
145     private List<String> activeProfileIds = Collections.emptyList();
146     private List<String> inactiveProfileIds = Collections.emptyList();
147     private Map<String, String> systemProperties = Collections.emptyMap();
148     private Map<String, String> userProperties = Collections.emptyMap();
149     private Model model;
150 
151     private final ThreadLocal<Record> records = new ThreadLocal<>();
152 
153     public DefaultProfileActivationContext(
154             PathTranslator pathTranslator, RootLocator rootLocator, Interpolator interpolator) {
155         this.pathTranslator = pathTranslator;
156         this.rootLocator = rootLocator;
157         this.interpolator = interpolator;
158     }
159 
160     Record start() {
161         Record record = records.get();
162         records.set(new Record());
163         return record;
164     }
165 
166     Record stop(Record previous) {
167         Record record = records.get();
168         records.set(previous);
169         // only keep keys for which the value is `true`
170         record.usedActiveProfiles.values().removeIf(value -> !value);
171         record.usedInactiveProfiles.values().removeIf(value -> !value);
172         return record;
173     }
174 
175     @Override
176     public boolean isProfileActive(String profileId) {
177         Record record = records.get();
178         if (record != null) {
179             return record.usedActiveProfiles.computeIfAbsent(profileId, activeProfileIds::contains);
180         } else {
181             return activeProfileIds.contains(profileId);
182         }
183     }
184 
185     /**
186      * Sets the identifiers of those profiles that should be activated by explicit demand.
187      *
188      * @param activeProfileIds The identifiers of those profiles to activate, may be {@code null}.
189      * @return This context, never {@code null}.
190      */
191     public DefaultProfileActivationContext setActiveProfileIds(List<String> activeProfileIds) {
192         this.activeProfileIds = unmodifiable(activeProfileIds);
193         return this;
194     }
195 
196     @Override
197     public boolean isProfileInactive(String profileId) {
198         Record record = records.get();
199         if (record != null) {
200             return record.usedInactiveProfiles.computeIfAbsent(profileId, inactiveProfileIds::contains);
201         } else {
202             return inactiveProfileIds.contains(profileId);
203         }
204     }
205 
206     /**
207      * Sets the identifiers of those profiles that should be deactivated by explicit demand.
208      *
209      * @param inactiveProfileIds The identifiers of those profiles to deactivate, may be {@code null}.
210      * @return This context, never {@code null}.
211      */
212     public DefaultProfileActivationContext setInactiveProfileIds(List<String> inactiveProfileIds) {
213         this.inactiveProfileIds = unmodifiable(inactiveProfileIds);
214         return this;
215     }
216 
217     @Override
218     public String getSystemProperty(String key) {
219         Record record = records.get();
220         if (record != null) {
221             return record.usedSystemProperties.computeIfAbsent(key, systemProperties::get);
222         } else {
223             return systemProperties.get(key);
224         }
225     }
226 
227     /**
228      * Sets the system properties to use for interpolation and profile activation. The system properties are collected
229      * from the runtime environment like {@link System#getProperties()} and environment variables.
230      *
231      * @param systemProperties The system properties, may be {@code null}.
232      * @return This context, never {@code null}.
233      */
234     public DefaultProfileActivationContext setSystemProperties(Map<String, String> systemProperties) {
235         this.systemProperties = unmodifiable(systemProperties);
236         return this;
237     }
238 
239     @Override
240     public String getUserProperty(String key) {
241         Record record = records.get();
242         if (record != null) {
243             return record.usedUserProperties.computeIfAbsent(key, userProperties::get);
244         } else {
245             return userProperties.get(key);
246         }
247     }
248 
249     /**
250      * Sets the user properties to use for interpolation and profile activation. The user properties have been
251      * configured directly by the user on his discretion, e.g. via the {@code -Dkey=value} parameter on the command
252      * line.
253      *
254      * @param userProperties The user properties, may be {@code null}.
255      * @return This context, never {@code null}.
256      */
257     public DefaultProfileActivationContext setUserProperties(Map<String, String> userProperties) {
258         this.userProperties = unmodifiable(userProperties);
259         return this;
260     }
261 
262     @Override
263     public String getModelArtifactId() {
264         Record record = records.get();
265         if (record != null) {
266             return record.usedModelInfos.computeIfAbsent(ModelInfo.ArtifactId, k -> model.getArtifactId());
267         } else {
268             return model.getArtifactId();
269         }
270     }
271 
272     @Override
273     public String getModelPackaging() {
274         Record record = records.get();
275         if (record != null) {
276             return record.usedModelInfos.computeIfAbsent(ModelInfo.Packaging, k -> model.getPackaging());
277         } else {
278             return model.getPackaging();
279         }
280     }
281 
282     @Override
283     public String getModelProperty(String key) {
284         Record record = records.get();
285         if (record != null) {
286             return record.usedModelProperties.computeIfAbsent(
287                     key, k -> model.getProperties().get(k));
288         } else {
289             return model.getProperties().get(key);
290         }
291     }
292 
293     @Override
294     public String getModelBaseDirectory() {
295         Record record = records.get();
296         if (record != null) {
297             return record.usedModelInfos.computeIfAbsent(ModelInfo.BaseDirectory, k -> doGetModelBaseDirectory());
298         } else {
299             return doGetModelBaseDirectory();
300         }
301     }
302 
303     private String doGetModelBaseDirectory() {
304         Path basedir = model.getProjectDirectory();
305         return basedir != null ? basedir.toAbsolutePath().toString() : null;
306     }
307 
308     @Override
309     public String getModelRootDirectory() {
310         Record record = records.get();
311         if (record != null) {
312             return record.usedModelInfos.computeIfAbsent(ModelInfo.RootDirectory, k -> doGetModelRootDirectory());
313         } else {
314             return doGetModelRootDirectory();
315         }
316     }
317 
318     private String doGetModelRootDirectory() {
319         Path basedir = model != null ? model.getProjectDirectory() : null;
320         Path rootdir = rootLocator != null ? rootLocator.findRoot(basedir) : null;
321         return rootdir != null ? rootdir.toAbsolutePath().toString() : null;
322     }
323 
324     public DefaultProfileActivationContext setModel(Model model) {
325         this.model = model;
326         return this;
327     }
328 
329     @Override
330     public String interpolatePath(String path) throws InterpolatorException {
331         if (path == null) {
332             return null;
333         }
334         String absolutePath = interpolator.interpolate(path, s -> {
335             if ("basedir".equals(s) || "project.basedir".equals(s)) {
336                 return getModelBaseDirectory();
337             }
338             if ("project.rootDirectory".equals(s)) {
339                 return getModelRootDirectory();
340             }
341             String r = getModelProperty(s);
342             if (r == null) {
343                 r = getUserProperty(s);
344             }
345             if (r == null) {
346                 r = getSystemProperty(s);
347             }
348             return r;
349         });
350         return pathTranslator.alignToBaseDirectory(absolutePath, model.getProjectDirectory());
351     }
352 
353     @Override
354     public boolean exists(String path, boolean enableGlob) throws ModelBuilderException {
355         Record record = records.get();
356         if (record != null) {
357             return record.usedExists.computeIfAbsent(
358                     new ExistRequest(path, enableGlob), r -> doExists(r.path, r.enableGlob));
359         } else {
360             return doExists(path, enableGlob);
361         }
362     }
363 
364     private boolean doExists(String path, boolean enableGlob) throws ModelBuilderException {
365         String pattern = interpolatePath(path);
366         String fixed, glob;
367         if (enableGlob) {
368             int asteriskIndex = pattern.indexOf('*');
369             int questionMarkIndex = pattern.indexOf('?');
370             int firstWildcardIndex = questionMarkIndex < 0
371                     ? asteriskIndex
372                     : asteriskIndex < 0 ? questionMarkIndex : Math.min(asteriskIndex, questionMarkIndex);
373             if (firstWildcardIndex < 0) {
374                 fixed = pattern;
375                 glob = "";
376             } else {
377                 int lastSep = pattern.substring(0, firstWildcardIndex).lastIndexOf(File.separatorChar);
378                 if (lastSep < 0) {
379                     fixed = "";
380                     glob = pattern;
381                 } else {
382                     fixed = pattern.substring(0, lastSep);
383                     glob = pattern.substring(lastSep + 1);
384                 }
385             }
386         } else {
387             fixed = pattern;
388             glob = "";
389         }
390         Path fixedPath = Paths.get(fixed);
391         return doExists(fixedPath, glob);
392     }
393 
394     private static Boolean doExists(Path fixedPath, String glob) {
395         if (fixedPath == null || !Files.exists(fixedPath)) {
396             return false;
397         }
398         if (glob != null && !glob.isEmpty()) {
399             try {
400                 PathMatcher matcher = fixedPath.getFileSystem().getPathMatcher("glob:" + glob);
401                 AtomicBoolean found = new AtomicBoolean(false);
402                 Files.walkFileTree(fixedPath, new SimpleFileVisitor<>() {
403                     @Override
404                     public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
405                         if (found.get() || matcher.matches(fixedPath.relativize(file))) {
406                             found.set(true);
407                             return FileVisitResult.TERMINATE;
408                         }
409                         return FileVisitResult.CONTINUE;
410                     }
411                 });
412                 return found.get();
413             } catch (IOException e) {
414                 throw new ProjectBuilderException(
415                         "Unable to verify file existence for '" + glob + "' inside '" + fixedPath + "'", e);
416             }
417         }
418         return true;
419     }
420 
421     private static List<String> unmodifiable(List<String> list) {
422         return list != null ? Collections.unmodifiableList(list) : Collections.emptyList();
423     }
424 
425     private static Map<String, String> unmodifiable(Map<String, String> map) {
426         return map != null ? Collections.unmodifiableMap(map) : Collections.emptyMap();
427     }
428 }