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.profile;
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.List;
31  import java.util.concurrent.atomic.AtomicBoolean;
32  
33  import org.apache.maven.api.services.VersionParser;
34  import org.apache.maven.api.services.model.ProfileActivationContext;
35  import org.apache.maven.internal.impl.model.ProfileActivationFilePathInterpolator;
36  
37  import static org.apache.maven.internal.impl.model.profile.ConditionParser.toInt;
38  
39  /**
40   * Provides a set of functions for evaluating profile activation conditions.
41   * These functions can be used in profile activation expressions to determine
42   * whether a profile should be activated based on various criteria.
43   */
44  @SuppressWarnings("unused")
45  public class ConditionFunctions {
46      private final ProfileActivationContext context;
47      private final VersionParser versionParser;
48      private final ProfileActivationFilePathInterpolator interpolator;
49  
50      /**
51       * Constructs a new ConditionFunctions instance.
52       *
53       * @param context The profile activation context
54       * @param versionParser The version parser for comparing versions
55       * @param interpolator The interpolator for resolving file paths
56       */
57      public ConditionFunctions(
58              ProfileActivationContext context,
59              VersionParser versionParser,
60              ProfileActivationFilePathInterpolator interpolator) {
61          this.context = context;
62          this.versionParser = versionParser;
63          this.interpolator = interpolator;
64      }
65  
66      /**
67       * Returns the length of the given string.
68       *
69       * @param args A list containing a single string argument
70       * @return The length of the string
71       * @throws IllegalArgumentException if the number of arguments is not exactly one
72       */
73      public Object length(List<Object> args) {
74          if (args.size() != 1) {
75              throw new IllegalArgumentException("length function requires exactly one argument");
76          }
77          String s = ConditionParser.toString(args.get(0));
78          return s.length();
79      }
80  
81      /**
82       * Converts the given string to uppercase.
83       *
84       * @param args A list containing a single string argument
85       * @return The uppercase version of the input string
86       * @throws IllegalArgumentException if the number of arguments is not exactly one
87       */
88      public Object upper(List<Object> args) {
89          if (args.size() != 1) {
90              throw new IllegalArgumentException("upper function requires exactly one argument");
91          }
92          String s = ConditionParser.toString(args.get(0));
93          return s.toUpperCase();
94      }
95  
96      /**
97       * Converts the given string to lowercase.
98       *
99       * @param args A list containing a single string argument
100      * @return The lowercase version of the input string
101      * @throws IllegalArgumentException if the number of arguments is not exactly one
102      */
103     public Object lower(List<Object> args) {
104         if (args.size() != 1) {
105             throw new IllegalArgumentException("lower function requires exactly one argument");
106         }
107         String s = ConditionParser.toString(args.get(0));
108         return s.toLowerCase();
109     }
110 
111     /**
112      * Returns a substring of the given string.
113      *
114      * @param args A list containing 2 or 3 arguments: the string, start index, and optionally end index
115      * @return The substring
116      * @throws IllegalArgumentException if the number of arguments is not 2 or 3
117      */
118     public Object substring(List<Object> args) {
119         if (args.size() < 2 || args.size() > 3) {
120             throw new IllegalArgumentException("substring function requires 2 or 3 arguments");
121         }
122         String s = ConditionParser.toString(args.get(0));
123         int start = toInt(args.get(1));
124         int end = args.size() == 3 ? toInt(args.get(2)) : s.length();
125         return s.substring(start, end);
126     }
127 
128     /**
129      * Finds the index of a substring within a string.
130      *
131      * @param args A list containing two strings: the main string and the substring to find
132      * @return The index of the substring, or -1 if not found
133      * @throws IllegalArgumentException if the number of arguments is not exactly two
134      */
135     public Object indexOf(List<Object> args) {
136         if (args.size() != 2) {
137             throw new IllegalArgumentException("indexOf function requires exactly two arguments");
138         }
139         String s = ConditionParser.toString(args.get(0));
140         String substring = ConditionParser.toString(args.get(1));
141         return s.indexOf(substring);
142     }
143 
144     /**
145      * Checks if a string contains a given substring.
146      *
147      * @param args A list containing two strings: the main string and the substring to check
148      * @return true if the main string contains the substring, false otherwise
149      * @throws IllegalArgumentException if the number of arguments is not exactly two
150      */
151     public Object contains(List<Object> args) {
152         if (args.size() != 2) {
153             throw new IllegalArgumentException("contains function requires exactly two arguments");
154         }
155         String s = ConditionParser.toString(args.get(0));
156         String substring = ConditionParser.toString(args.get(1));
157         return s.contains(substring);
158     }
159 
160     /**
161      * Checks if a string matches a given regular expression.
162      *
163      * @param args A list containing two strings: the string to check and the regex pattern
164      * @return true if the string matches the regex, false otherwise
165      * @throws IllegalArgumentException if the number of arguments is not exactly two
166      */
167     public Object matches(List<Object> args) {
168         if (args.size() != 2) {
169             throw new IllegalArgumentException("matches function requires exactly two arguments");
170         }
171         String s = ConditionParser.toString(args.get(0));
172         String regex = ConditionParser.toString(args.get(1));
173         return s.matches(regex);
174     }
175 
176     /**
177      * Negates a boolean value.
178      *
179      * @param args A list containing a single boolean argument
180      * @return The negation of the input boolean
181      * @throws IllegalArgumentException if the number of arguments is not exactly one
182      */
183     public Object not(List<Object> args) {
184         if (args.size() != 1) {
185             throw new IllegalArgumentException("not function requires exactly one argument");
186         }
187         return !ConditionParser.toBoolean(args.get(0));
188     }
189 
190     /**
191      * Implements an if-then-else operation.
192      *
193      * @param args A list containing three arguments: condition, value if true, value if false
194      * @return The second argument if the condition is true, the third argument otherwise
195      * @throws IllegalArgumentException if the number of arguments is not exactly three
196      */
197     @SuppressWarnings("checkstyle:MethodName")
198     public Object if_(List<Object> args) {
199         if (args.size() != 3) {
200             throw new IllegalArgumentException("if function requires exactly three arguments");
201         }
202         boolean condition = ConditionParser.toBoolean(args.get(0));
203         return condition ? args.get(1) : args.get(2);
204     }
205 
206     /**
207      * Checks if a file or directory exists at the given path.
208      *
209      * @param args A list containing a single string argument representing the path
210      * @return true if the file or directory exists, false otherwise
211      * @throws IllegalArgumentException if the number of arguments is not exactly one
212      * @throws IOException if a problem occurs while walking the file system
213      */
214     public Object exists(List<Object> args) throws IOException {
215         if (args.size() != 1) {
216             throw new IllegalArgumentException("exists function requires exactly one argument");
217         }
218         String path = ConditionParser.toString(args.get(0));
219         return fileExists(path);
220     }
221 
222     /**
223      * Checks if a file or directory is missing at the given path.
224      *
225      * @param args A list containing a single string argument representing the path
226      * @return true if the file or directory does not exist, false otherwise
227      * @throws IllegalArgumentException if the number of arguments is not exactly one
228      * @throws IOException if a problem occurs while walking the file system
229      */
230     public Object missing(List<Object> args) throws IOException {
231         if (args.size() != 1) {
232             throw new IllegalArgumentException("missing function requires exactly one argument");
233         }
234         String path = ConditionParser.toString(args.get(0));
235         return !fileExists(path);
236     }
237 
238     private boolean fileExists(String path) throws IOException {
239         String pattern = interpolator.interpolate(path, context);
240         int asteriskIndex = pattern.indexOf('*');
241         int questionMarkIndex = pattern.indexOf('?');
242         int firstWildcardIndex = questionMarkIndex < 0
243                 ? asteriskIndex
244                 : asteriskIndex < 0 ? questionMarkIndex : Math.min(asteriskIndex, questionMarkIndex);
245         String fixed, glob;
246         if (firstWildcardIndex < 0) {
247             fixed = pattern;
248             glob = "";
249         } else {
250             int lastSep = pattern.substring(0, firstWildcardIndex).lastIndexOf(File.separatorChar);
251             if (lastSep < 0) {
252                 fixed = "";
253                 glob = pattern;
254             } else {
255                 fixed = pattern.substring(0, lastSep);
256                 glob = pattern.substring(lastSep + 1);
257             }
258         }
259         Path fixedPath = Paths.get(fixed);
260         if (!Files.exists(fixedPath)) {
261             return false;
262         }
263         if (!glob.isEmpty()) {
264             PathMatcher matcher = fixedPath.getFileSystem().getPathMatcher("glob:" + glob);
265             AtomicBoolean found = new AtomicBoolean(false);
266             Files.walkFileTree(fixedPath, new SimpleFileVisitor<>() {
267                 @Override
268                 public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
269                     if (found.get() || matcher.matches(fixedPath.relativize(file))) {
270                         found.set(true);
271                         return FileVisitResult.TERMINATE;
272                     }
273                     return FileVisitResult.CONTINUE;
274                 }
275             });
276             return found.get();
277         }
278         return true;
279     }
280 
281     /**
282      * Checks if a version is within a specified version range.
283      *
284      * @param args A list containing two strings: the version to check and the version range
285      * @return true if the version is within the range, false otherwise
286      * @throws IllegalArgumentException if the number of arguments is not exactly two
287      */
288     public Object inrange(List<Object> args) {
289         if (args.size() != 2) {
290             throw new IllegalArgumentException("inrange function requires exactly two arguments");
291         }
292         String version = ConditionParser.toString(args.get(0));
293         String range = ConditionParser.toString(args.get(1));
294         return versionParser.parseVersionRange(range).contains(versionParser.parseVersion(version));
295     }
296 }