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.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          final Map<String, Boolean> usedActiveProfiles;
67          final Map<String, Boolean> usedInactiveProfiles;
68          final Map<String, String> usedSystemProperties;
69          final Map<String, String> usedUserProperties;
70          final Map<String, String> usedModelProperties;
71          final Map<ModelInfo, String> usedModelInfos;
72          final Map<ExistRequest, Boolean> usedExists;
73  
74          // Constructor for recording phase - creates mutable maps
75          Record() {
76              this.usedActiveProfiles = new HashMap<>();
77              this.usedInactiveProfiles = new HashMap<>();
78              this.usedSystemProperties = new HashMap<>();
79              this.usedUserProperties = new HashMap<>();
80              this.usedModelProperties = new HashMap<>();
81              this.usedModelInfos = new HashMap<>();
82              this.usedExists = new HashMap<>();
83          }
84  
85          // Copy constructor for caching phase - creates immutable maps
86          Record(Record source) {
87              this.usedActiveProfiles = Map.copyOf(source.usedActiveProfiles);
88              this.usedInactiveProfiles = Map.copyOf(source.usedInactiveProfiles);
89              this.usedSystemProperties = Map.copyOf(source.usedSystemProperties);
90              this.usedUserProperties = Map.copyOf(source.usedUserProperties);
91              this.usedModelProperties = Map.copyOf(source.usedModelProperties);
92              this.usedModelInfos = Map.copyOf(source.usedModelInfos);
93              this.usedExists = Map.copyOf(source.usedExists);
94          }
95  
96          @Override
97          public boolean equals(Object o) {
98              if (o instanceof Record record) {
99                  return Objects.equals(usedActiveProfiles, record.usedActiveProfiles)
100                         && Objects.equals(usedInactiveProfiles, record.usedInactiveProfiles)
101                         && Objects.equals(usedSystemProperties, record.usedSystemProperties)
102                         && Objects.equals(usedUserProperties, record.usedUserProperties)
103                         && Objects.equals(usedModelProperties, record.usedModelProperties)
104                         && Objects.equals(usedModelInfos, record.usedModelInfos)
105                         && Objects.equals(usedExists, record.usedExists);
106             }
107             return false;
108         }
109 
110         @Override
111         public int hashCode() {
112             return Objects.hash(
113                     usedActiveProfiles,
114                     usedInactiveProfiles,
115                     usedSystemProperties,
116                     usedUserProperties,
117                     usedModelProperties,
118                     usedModelInfos,
119                     usedExists);
120         }
121 
122         boolean matches(DefaultProfileActivationContext context) {
123             return matchesProfiles(usedActiveProfiles, context.activeProfileIds)
124                     && matchesProfiles(usedInactiveProfiles, context.inactiveProfileIds)
125                     && matchesProperties(usedSystemProperties, context.systemProperties)
126                     && matchesProperties(usedUserProperties, context.userProperties)
127                     && matchesProperties(usedModelProperties, context.model.getProperties())
128                     && matchesModelInfos(usedModelInfos, context)
129                     && matchesExists(usedExists, context);
130         }
131 
132         private boolean matchesProfiles(Map<String, Boolean> expected, List<String> actual) {
133             return expected.entrySet().stream()
134                     .allMatch(e -> Objects.equals(e.getValue(), actual.contains(e.getKey())));
135         }
136 
137         private boolean matchesProperties(Map<String, String> expected, Map<String, String> actual) {
138             return expected.entrySet().stream().allMatch(e -> Objects.equals(e.getValue(), actual.get(e.getKey())));
139         }
140 
141         private boolean matchesModelInfos(Map<ModelInfo, String> infos, DefaultProfileActivationContext context) {
142             return infos.entrySet().stream()
143                     .allMatch(e -> Objects.equals(e.getValue(), getModelValue(e.getKey(), context)));
144         }
145 
146         private String getModelValue(ModelInfo key, DefaultProfileActivationContext context) {
147             return switch (key) {
148                 case ArtifactId -> context.model.getArtifactId();
149                 case Packaging -> context.model.getPackaging();
150                 case BaseDirectory -> context.doGetModelBaseDirectory();
151                 case RootDirectory -> context.doGetModelRootDirectory();
152             };
153         }
154 
155         private boolean matchesExists(Map<ExistRequest, Boolean> exists, DefaultProfileActivationContext context) {
156             return exists.entrySet().stream()
157                     .allMatch(e -> Objects.equals(
158                             e.getValue(),
159                             context.doExists(e.getKey().path(), e.getKey().enableGlob())));
160         }
161     }
162 
163     private final PathTranslator pathTranslator;
164     private final RootLocator rootLocator;
165     private final Interpolator interpolator;
166 
167     private List<String> activeProfileIds = Collections.emptyList();
168     private List<String> inactiveProfileIds = Collections.emptyList();
169     private Map<String, String> systemProperties = Collections.emptyMap();
170     private Map<String, String> userProperties = Collections.emptyMap();
171     private Model model;
172     final Record record;
173 
174     public DefaultProfileActivationContext(
175             PathTranslator pathTranslator, RootLocator rootLocator, Interpolator interpolator) {
176         this.pathTranslator = pathTranslator;
177         this.rootLocator = rootLocator;
178         this.interpolator = interpolator;
179         this.record = null;
180     }
181 
182     @SuppressWarnings("checkstyle:ParameterNumber")
183     public DefaultProfileActivationContext(
184             PathTranslator pathTranslator,
185             RootLocator rootLocator,
186             Interpolator interpolator,
187             List<String> activeProfileIds,
188             List<String> inactiveProfileIds,
189             Map<String, String> systemProperties,
190             Map<String, String> userProperties,
191             Model model) {
192         this(
193                 pathTranslator,
194                 rootLocator,
195                 interpolator,
196                 activeProfileIds,
197                 inactiveProfileIds,
198                 systemProperties,
199                 userProperties,
200                 model,
201                 null);
202     }
203 
204     @SuppressWarnings("checkstyle:ParameterNumber")
205     private DefaultProfileActivationContext(
206             PathTranslator pathTranslator,
207             RootLocator rootLocator,
208             Interpolator interpolator,
209             List<String> activeProfileIds,
210             List<String> inactiveProfileIds,
211             Map<String, String> systemProperties,
212             Map<String, String> userProperties,
213             Model model,
214             Record record) {
215         this.pathTranslator = pathTranslator;
216         this.rootLocator = rootLocator;
217         this.interpolator = interpolator;
218         this.activeProfileIds = activeProfileIds;
219         this.inactiveProfileIds = inactiveProfileIds;
220         this.systemProperties = systemProperties;
221         this.userProperties = userProperties;
222         this.model = model;
223         this.record = record;
224     }
225 
226     DefaultProfileActivationContext start() {
227         return new DefaultProfileActivationContext(
228                 pathTranslator,
229                 rootLocator,
230                 interpolator,
231                 activeProfileIds,
232                 inactiveProfileIds,
233                 systemProperties,
234                 userProperties,
235                 model,
236                 new Record());
237     }
238 
239     Record stop() {
240         // only keep keys for which the value is `true`
241         Objects.requireNonNull(record, "start() must be called before stop()");
242         record.usedActiveProfiles.values().removeIf(value -> !value);
243         record.usedInactiveProfiles.values().removeIf(value -> !value);
244         return new Record(record); // Return immutable copy for thread-safe caching
245     }
246 
247     @Override
248     public boolean isProfileActive(String profileId) {
249         if (record != null) {
250             return record.usedActiveProfiles.computeIfAbsent(profileId, activeProfileIds::contains);
251         } else {
252             return activeProfileIds.contains(profileId);
253         }
254     }
255 
256     @Override
257     public boolean isProfileInactive(String profileId) {
258         if (record != null) {
259             return record.usedInactiveProfiles.computeIfAbsent(profileId, inactiveProfileIds::contains);
260         } else {
261             return inactiveProfileIds.contains(profileId);
262         }
263     }
264 
265     @Override
266     public String getSystemProperty(String key) {
267         if (record != null) {
268             return record.usedSystemProperties.computeIfAbsent(key, systemProperties::get);
269         } else {
270             return systemProperties.get(key);
271         }
272     }
273 
274     /**
275      * Sets the system properties to use for interpolation and profile activation. The system properties are collected
276      * from the runtime environment like {@link System#getProperties()} and environment variables.
277      *
278      * @param systemProperties The system properties, may be {@code null}.
279      * @return This context, never {@code null}.
280      */
281     public DefaultProfileActivationContext setSystemProperties(Map<String, String> systemProperties) {
282         this.systemProperties = unmodifiable(systemProperties);
283         return this;
284     }
285 
286     @Override
287     public String getUserProperty(String key) {
288         if (record != null) {
289             return record.usedUserProperties.computeIfAbsent(key, userProperties::get);
290         } else {
291             return userProperties.get(key);
292         }
293     }
294 
295     /**
296      * Sets the user properties to use for interpolation and profile activation. The user properties have been
297      * configured directly by the user on his discretion, e.g. via the {@code -Dkey=value} parameter on the command
298      * line.
299      *
300      * @param userProperties The user properties, may be {@code null}.
301      * @return This context, never {@code null}.
302      */
303     public DefaultProfileActivationContext setUserProperties(Map<String, String> userProperties) {
304         this.userProperties = unmodifiable(userProperties);
305         return this;
306     }
307 
308     @Override
309     public String getModelArtifactId() {
310         if (record != null) {
311             return record.usedModelInfos.computeIfAbsent(ModelInfo.ArtifactId, k -> model.getArtifactId());
312         } else {
313             return model.getArtifactId();
314         }
315     }
316 
317     @Override
318     public String getModelPackaging() {
319         if (record != null) {
320             return record.usedModelInfos.computeIfAbsent(ModelInfo.Packaging, k -> model.getPackaging());
321         } else {
322             return model.getPackaging();
323         }
324     }
325 
326     @Override
327     public String getModelProperty(String key) {
328         if (record != null) {
329             return record.usedModelProperties.computeIfAbsent(
330                     key, k -> model.getProperties().get(k));
331         } else {
332             return model.getProperties().get(key);
333         }
334     }
335 
336     @Override
337     public String getModelBaseDirectory() {
338         if (record != null) {
339             return record.usedModelInfos.computeIfAbsent(ModelInfo.BaseDirectory, k -> doGetModelBaseDirectory());
340         } else {
341             return doGetModelBaseDirectory();
342         }
343     }
344 
345     public String doGetModelBaseDirectory() {
346         Path basedir = model.getProjectDirectory();
347         return basedir != null ? basedir.toAbsolutePath().toString() : null;
348     }
349 
350     @Override
351     public String getModelRootDirectory() {
352         if (record != null) {
353             return record.usedModelInfos.computeIfAbsent(ModelInfo.RootDirectory, k -> doGetModelRootDirectory());
354         } else {
355             return doGetModelRootDirectory();
356         }
357     }
358 
359     private String doGetModelRootDirectory() {
360         Path basedir = model != null ? model.getProjectDirectory() : null;
361         Path rootdir = rootLocator != null ? rootLocator.findRoot(basedir) : null;
362         return rootdir != null ? rootdir.toAbsolutePath().toString() : null;
363     }
364 
365     public DefaultProfileActivationContext setModel(Model model) {
366         this.model = model;
367         return this;
368     }
369 
370     @Override
371     public String interpolatePath(String path) throws InterpolatorException {
372         if (path == null) {
373             return null;
374         }
375         String absolutePath = interpolator.interpolate(path, s -> {
376             if ("basedir".equals(s) || "project.basedir".equals(s)) {
377                 return getModelBaseDirectory();
378             }
379             if ("project.rootDirectory".equals(s)) {
380                 return getModelRootDirectory();
381             }
382             String r = getModelProperty(s);
383             if (r == null) {
384                 r = getUserProperty(s);
385             }
386             if (r == null) {
387                 r = getSystemProperty(s);
388             }
389             return r;
390         });
391         return pathTranslator.alignToBaseDirectory(absolutePath, model.getProjectDirectory());
392     }
393 
394     @Override
395     public boolean exists(String path, boolean enableGlob) throws ModelBuilderException {
396         if (record != null) {
397             return record.usedExists.computeIfAbsent(
398                     new ExistRequest(path, enableGlob), r -> doExists(r.path, r.enableGlob));
399         } else {
400             return doExists(path, enableGlob);
401         }
402     }
403 
404     private boolean doExists(String path, boolean enableGlob) throws ModelBuilderException {
405         String pattern = interpolatePath(path);
406         String fixed, glob;
407         if (enableGlob) {
408             int asteriskIndex = pattern.indexOf('*');
409             int questionMarkIndex = pattern.indexOf('?');
410             int firstWildcardIndex = questionMarkIndex < 0
411                     ? asteriskIndex
412                     : asteriskIndex < 0 ? questionMarkIndex : Math.min(asteriskIndex, questionMarkIndex);
413             if (firstWildcardIndex < 0) {
414                 fixed = pattern;
415                 glob = "";
416             } else {
417                 int lastSep = pattern.substring(0, firstWildcardIndex).lastIndexOf(File.separatorChar);
418                 if (lastSep < 0) {
419                     fixed = "";
420                     glob = pattern;
421                 } else {
422                     fixed = pattern.substring(0, lastSep);
423                     glob = pattern.substring(lastSep + 1);
424                 }
425             }
426         } else {
427             fixed = pattern;
428             glob = "";
429         }
430         Path fixedPath = Paths.get(fixed);
431         return doExists(fixedPath, glob);
432     }
433 
434     private static Boolean doExists(Path fixedPath, String glob) {
435         if (fixedPath == null || !Files.exists(fixedPath)) {
436             return false;
437         }
438         if (glob != null && !glob.isEmpty()) {
439             try {
440                 PathMatcher matcher = fixedPath.getFileSystem().getPathMatcher("glob:" + glob);
441                 AtomicBoolean found = new AtomicBoolean(false);
442                 Files.walkFileTree(fixedPath, new SimpleFileVisitor<>() {
443                     @Override
444                     public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
445                         if (found.get() || matcher.matches(fixedPath.relativize(file))) {
446                             found.set(true);
447                             return FileVisitResult.TERMINATE;
448                         }
449                         return FileVisitResult.CONTINUE;
450                     }
451                 });
452                 return found.get();
453             } catch (IOException e) {
454                 throw new ProjectBuilderException(
455                         "Unable to verify file existence for '" + glob + "' inside '" + fixedPath + "'", e);
456             }
457         }
458         return true;
459     }
460 
461     private static Map<String, String> unmodifiable(Map<String, String> map) {
462         return map != null ? Collections.unmodifiableMap(map) : Collections.emptyMap();
463     }
464 }