1 package org.apache.archiva.common.utils;
4 * Licensed to the Apache Software Foundation (ASF) under one
5 * or more contributor license agreements. See the NOTICE file
6 * distributed with this work for additional information
7 * regarding copyright ownership. The ASF licenses this file
8 * to you under the Apache License, Version 2.0 (the
9 * "License"); you may not use this file except in compliance
10 * with the License. You may obtain a copy of the License at
12 * http://www.apache.org/licenses/LICENSE-2.0
14 * Unless required by applicable law or agreed to in writing,
15 * software distributed under the License is distributed on an
16 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17 * KIND, either express or implied. See the License for the
18 * specific language governing permissions and limitations
22 import org.apache.commons.lang3.StringUtils;
24 import java.net.MalformedURLException;
26 import java.nio.file.FileSystems;
27 import java.nio.file.Path;
28 import java.nio.file.Paths;
29 import java.util.ArrayList;
30 import java.util.List;
31 import java.util.StringTokenizer;
32 import java.util.Vector;
33 import java.util.stream.StreamSupport;
37 * PathUtil - simple utility methods for path manipulation.
39 * Some code is from Apache Ant SelectorUtils
45 public static final String DEEP_TREE_MATCH = "**";
47 public static String toUrl( String path )
49 // Is our work already done for us?
50 if ( path.startsWith( "file:/" ) )
55 return toUrl( Paths.get( path ) );
58 public static String toUrl( Path file )
62 return file.toUri().toURL().toExternalForm();
64 catch ( MalformedURLException e )
66 String pathCorrected = StringUtils.replaceChars( file.toAbsolutePath().toString(), '\\', '/' );
67 if ( pathCorrected.startsWith( "file:/" ) )
72 return "file://" + pathCorrected;
77 * Given a basedir and a child file, return the relative path to the child.
79 * @param basedir the basedir.
80 * @param file the file to get the relative path for.
81 * @return the relative path to the child. (NOTE: this path will NOT start with a file separator character)
83 public static String getRelative( Path basedir, Path file )
85 if (basedir.isAbsolute() && !file.isAbsolute()) {
86 return basedir.normalize().relativize(file.toAbsolutePath()).toString();
87 } else if (!basedir.isAbsolute() && file.isAbsolute()) {
88 return basedir.toAbsolutePath().relativize(file.normalize()).toString();
90 return basedir.normalize().relativize(file.normalize()).toString();
94 public static String getRelative(String basedir, Path file) {
95 return getRelative(Paths.get(basedir), file);
99 * Given a basedir and a child file, return the relative path to the child.
101 * @param basedir the basedir.
102 * @param child the child path (can be a full path)
103 * @return the relative path to the child. (NOTE: this path will NOT start with a file separator character)
105 public static String getRelative( String basedir, String child )
108 return getRelative(basedir, Paths.get(child));
112 * Returns a path object from the given URI. If the URI has no scheme, the path of the URI is used
113 * for creating the filesystem path.
115 * @param uri the uri to convert
116 * @return a path object with the given path
117 * @throws java.nio.file.FileSystemNotFoundException if the uri scheme is not known.
119 public static Path getPathFromUri( URI uri) {
121 return Paths.get("");
122 } else if (uri.getScheme()==null) {
123 return Paths.get(uri.getPath());
125 return Paths.get(uri);
129 public static boolean isAbsolutePath(String path) {
132 return Paths.get( path ).isAbsolute( );
133 } catch (Exception e) {
138 public static char getSeparatorChar() {
139 return FileSystems.getDefault( ).getSeparator( ).charAt( 0 );
142 public static String[] dissect(String pathString) {
143 Path path = Paths.get(pathString);
144 return StreamSupport.stream(path.spliterator(), false).map(Path::toString)
145 .toArray(String[]::new);
148 public static String separatorsToUnix(String path)
150 return path != null && path.indexOf( 92 ) != -1 ? path.replace( '\\', '/' ) : path;
155 * Tests whether or not a given path matches a given pattern.
157 * If you need to call this method multiple times with the same
158 * pattern you should rather use TokenizedPath
160 * @param pattern The pattern to match against. Must not be
162 * @param str The path to match, as a String. Must not be
165 * @return <code>true</code> if the pattern matches against the string,
166 * or <code>false</code> otherwise.
168 public static boolean matchPath(String pattern, String str) {
169 String[] patDirs = tokenizePathAsArray(pattern);
170 return matchPath(patDirs, tokenizePathAsArray(str), true);
174 * Tests whether or not a given path matches a given pattern.
176 * If you need to call this method multiple times with the same
177 * pattern you should rather use TokenizedPattern
180 * @param pattern The pattern to match against. Must not be
182 * @param str The path to match, as a String. Must not be
184 * @param isCaseSensitive Whether or not matching should be performed
187 * @return <code>true</code> if the pattern matches against the string,
188 * or <code>false</code> otherwise.
190 public static boolean matchPath(String pattern, String str,
191 boolean isCaseSensitive) {
192 String[] patDirs = tokenizePathAsArray(pattern);
193 return matchPath(patDirs, tokenizePathAsArray(str), isCaseSensitive);
197 * Core implementation of matchPath. It is isolated so that it
198 * can be called from TokenizedPattern.
200 static boolean matchPath(String[] tokenizedPattern, String[] strDirs,
201 boolean isCaseSensitive) {
203 int patIdxEnd = tokenizedPattern.length - 1;
205 int strIdxEnd = strDirs.length - 1;
208 while (patIdxStart <= patIdxEnd && strIdxStart <= strIdxEnd) {
209 String patDir = tokenizedPattern[patIdxStart];
210 if (patDir.equals(DEEP_TREE_MATCH)) {
213 if (!match(patDir, strDirs[strIdxStart], isCaseSensitive)) {
219 if (strIdxStart > strIdxEnd) {
220 // String is exhausted
221 for (int i = patIdxStart; i <= patIdxEnd; i++) {
222 if (!tokenizedPattern[i].equals(DEEP_TREE_MATCH)) {
228 if (patIdxStart > patIdxEnd) {
229 // String not exhausted, but pattern is. Failure.
234 while (patIdxStart <= patIdxEnd && strIdxStart <= strIdxEnd) {
235 String patDir = tokenizedPattern[patIdxEnd];
236 if (patDir.equals(DEEP_TREE_MATCH)) {
239 if (!match(patDir, strDirs[strIdxEnd], isCaseSensitive)) {
245 if (strIdxStart > strIdxEnd) {
246 // String is exhausted
247 for (int i = patIdxStart; i <= patIdxEnd; i++) {
248 if (!tokenizedPattern[i].equals(DEEP_TREE_MATCH)) {
255 while (patIdxStart != patIdxEnd && strIdxStart <= strIdxEnd) {
257 for (int i = patIdxStart + 1; i <= patIdxEnd; i++) {
258 if (tokenizedPattern[i].equals(DEEP_TREE_MATCH)) {
263 if (patIdxTmp == patIdxStart + 1) {
264 // '**/**' situation, so skip one
268 // Find the pattern between padIdxStart & padIdxTmp in str between
269 // strIdxStart & strIdxEnd
270 int patLength = (patIdxTmp - patIdxStart - 1);
271 int strLength = (strIdxEnd - strIdxStart + 1);
274 for (int i = 0; i <= strLength - patLength; i++) {
275 for (int j = 0; j < patLength; j++) {
276 String subPat = tokenizedPattern[patIdxStart + j + 1];
277 String subStr = strDirs[strIdxStart + i + j];
278 if (!match(subPat, subStr, isCaseSensitive)) {
282 foundIdx = strIdxStart + i;
285 if (foundIdx == -1) {
289 patIdxStart = patIdxTmp;
290 strIdxStart = foundIdx + patLength;
293 for (int i = patIdxStart; i <= patIdxEnd; i++) {
294 if (!DEEP_TREE_MATCH.equals(tokenizedPattern[i])) {
302 * Tests whether or not a string matches against a pattern.
303 * The pattern may contain two special characters:<br>
304 * '*' means zero or more characters<br>
305 * '?' means one and only one character
307 * @param pattern The pattern to match against.
308 * Must not be <code>null</code>.
309 * @param str The string which must be matched against the pattern.
310 * Must not be <code>null</code>.
312 * @return <code>true</code> if the string matches against the pattern,
313 * or <code>false</code> otherwise.
315 public static boolean match(String pattern, String str) {
316 return match(pattern, str, true);
320 * Tests whether or not a string matches against a pattern.
321 * The pattern may contain two special characters:<br>
322 * '*' means zero or more characters<br>
323 * '?' means one and only one character
325 * @param pattern The pattern to match against.
326 * Must not be <code>null</code>.
327 * @param str The string which must be matched against the pattern.
328 * Must not be <code>null</code>.
329 * @param caseSensitive Whether or not matching should be performed
333 * @return <code>true</code> if the string matches against the pattern,
334 * or <code>false</code> otherwise.
336 public static boolean match(String pattern, String str,
337 boolean caseSensitive) {
338 char[] patArr = pattern.toCharArray();
339 char[] strArr = str.toCharArray();
341 int patIdxEnd = patArr.length - 1;
343 int strIdxEnd = strArr.length - 1;
345 boolean containsStar = false;
346 for (char ch : patArr) {
354 // No '*'s, so we make a shortcut
355 if (patIdxEnd != strIdxEnd) {
356 return false; // Pattern and string do not have the same size
358 for (int i = 0; i <= patIdxEnd; i++) {
360 if (ch != '?' && different(caseSensitive, ch, strArr[i])) {
361 return false; // Character mismatch
364 return true; // String matches against pattern
367 if (patIdxEnd == 0) {
368 return true; // Pattern contains only '*', which matches anything
371 // Process characters before first star
373 char ch = patArr[patIdxStart];
374 if (ch == '*' || strIdxStart > strIdxEnd) {
378 && different(caseSensitive, ch, strArr[strIdxStart])) {
379 return false; // Character mismatch
384 if (strIdxStart > strIdxEnd) {
385 // All characters in the string are used. Check if only '*'s are
386 // left in the pattern. If so, we succeeded. Otherwise failure.
387 return allStars(patArr, patIdxStart, patIdxEnd);
390 // Process characters after last star
392 char ch = patArr[patIdxEnd];
393 if (ch == '*' || strIdxStart > strIdxEnd) {
396 if (ch != '?' && different(caseSensitive, ch, strArr[strIdxEnd])) {
397 return false; // Character mismatch
402 if (strIdxStart > strIdxEnd) {
403 // All characters in the string are used. Check if only '*'s are
404 // left in the pattern. If so, we succeeded. Otherwise failure.
405 return allStars(patArr, patIdxStart, patIdxEnd);
408 // process pattern between stars. padIdxStart and patIdxEnd point
410 while (patIdxStart != patIdxEnd && strIdxStart <= strIdxEnd) {
412 for (int i = patIdxStart + 1; i <= patIdxEnd; i++) {
413 if (patArr[i] == '*') {
418 if (patIdxTmp == patIdxStart + 1) {
419 // Two stars next to each other, skip the first one.
423 // Find the pattern between padIdxStart & padIdxTmp in str between
424 // strIdxStart & strIdxEnd
425 int patLength = (patIdxTmp - patIdxStart - 1);
426 int strLength = (strIdxEnd - strIdxStart + 1);
429 for (int i = 0; i <= strLength - patLength; i++) {
430 for (int j = 0; j < patLength; j++) {
431 char ch = patArr[patIdxStart + j + 1];
432 if (ch != '?' && different(caseSensitive, ch,
433 strArr[strIdxStart + i + j])) {
437 foundIdx = strIdxStart + i;
441 if (foundIdx == -1) {
444 patIdxStart = patIdxTmp;
445 strIdxStart = foundIdx + patLength;
448 // All characters in the string are used. Check if only '*'s are left
449 // in the pattern. If so, we succeeded. Otherwise failure.
450 return allStars(patArr, patIdxStart, patIdxEnd);
453 private static boolean allStars(char[] chars, int start, int end) {
454 for (int i = start; i <= end; ++i) {
455 if (chars[i] != '*') {
462 private static boolean different(
463 boolean caseSensitive, char ch, char other) {
466 : Character.toUpperCase(ch) != Character.toUpperCase(other);
470 * Breaks a path up into a Vector of path elements, tokenizing on
471 * <code>File.separator</code>.
473 * @param path Path to tokenize. Must not be <code>null</code>.
475 * @return a Vector of path elements from the tokenized path
477 public static Vector<String> tokenizePath( String path) {
478 return tokenizePath(path, FileSystems.getDefault( ).getSeparator());
482 * Breaks a path up into a Vector of path elements, tokenizing on
484 * @param path Path to tokenize. Must not be <code>null</code>.
485 * @param separator the separator against which to tokenize.
487 * @return a Vector of path elements from the tokenized path
490 public static Vector<String> tokenizePath(String path, String separator) {
491 Vector<String> ret = new Vector<>();
492 if (isAbsolutePath(path)) {
493 String[] s = dissect(path);
497 StringTokenizer st = new StringTokenizer(path, separator);
498 while (st.hasMoreTokens()) {
499 ret.addElement(st.nextToken());
505 * Same as {@link #tokenizePath tokenizePath} but hopefully faster.
508 static String[] tokenizePathAsArray(String path) {
510 if (isAbsolutePath(path)) {
511 String[] s = dissect(path);
515 char sep = getSeparatorChar();
517 int len = path.length();
519 for (int pos = 0; pos < len; pos++) {
520 if (path.charAt(pos) == sep) {
530 String[] l = new String[count + ((root == null) ? 0 : 1)];
539 for (int pos = 0; pos < len; pos++) {
540 if (path.charAt(pos) == sep) {
542 String tok = path.substring(start, pos);
549 String tok = path.substring(start);
550 l[count/*++*/] = tok;