001/*-------------------------------------------------------------------------+ 002| | 003| Copyright 2005-2011 The ConQAT Project | 004| | 005| Licensed under the Apache License, Version 2.0 (the "License"); | 006| you may not use this file except in compliance with the License. | 007| You may obtain a copy of the License at | 008| | 009| http://www.apache.org/licenses/LICENSE-2.0 | 010| | 011| Unless required by applicable law or agreed to in writing, software | 012| distributed under the License is distributed on an "AS IS" BASIS, | 013| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 014| See the License for the specific language governing permissions and | 015| limitations under the License. | 016+-------------------------------------------------------------------------*/ 017package org.conqat.engine.resource.util; 018 019import java.util.ArrayList; 020import java.util.Arrays; 021import java.util.List; 022import java.util.regex.Matcher; 023import java.util.regex.Pattern; 024 025import org.conqat.lib.commons.string.StringUtils; 026 027/** 028 * Utility methods for dealing with uniform paths. 029 */ 030public class UniformPathUtils { 031 032 /** 033 * Matches windows drive letters prefixes, e.g. "C:/", and captures the path 034 * after the drive letter as first group. Requires the path to be normalized 035 * before matching, as the pattern will not match '\'. 036 */ 037 private static final Pattern DRIVE_LETTER_PATTERN = Pattern.compile("[A-Za-z]:/(.*)"); 038 039 /** The character used as path separator in uniform paths. */ 040 public static final char SEPARATOR_CHAR = '/'; 041 042 /** String representation of {@link #SEPARATOR_CHAR}. */ 043 public static final String SEPARATOR = String.valueOf(SEPARATOR_CHAR); 044 045 /** Pattern used for splitting along the separator. */ 046 private static final Pattern SPLIT_PATTERN = Pattern.compile("(?<!\\\\)" + SEPARATOR); 047 048 /** 049 * Extracts the project part of a uniform path, which is everything up to the 050 * first {@link #SEPARATOR_CHAR}. 051 */ 052 public static String extractProject(String uniformPath) { 053 return StringUtils.getFirstParts(uniformPath, 1, SEPARATOR_CHAR); 054 } 055 056 /** 057 * Returns the path without the project, i.e. removes everything up to the first 058 * {@link #SEPARATOR_CHAR}. 059 */ 060 public static String stripProject(String uniformPath) { 061 int pos = uniformPath.indexOf(SEPARATOR_CHAR); 062 if (pos >= 0) { 063 return uniformPath.substring(pos + 1); 064 } 065 return uniformPath; 066 } 067 068 /** 069 * Returns the element name for a uniform path, which is the everything starting 070 * from the last {@link #SEPARATOR_CHAR}. 071 */ 072 public static String getElementName(String uniformPath) { 073 return StringUtils.getLastPart(uniformPath, SEPARATOR_CHAR); 074 } 075 076 /** 077 * Returns the parent path for a path which is everything up to the last 078 * non-escaped {@link #SEPARATOR_CHAR}. If no separator is found, the empty 079 * string is returned. 080 */ 081 public static String getParentPath(String uniformPath) { 082 // we can not use StringUtils.removeLastPart(), as the behavior for a 083 // string without separator is different here 084 Matcher matcher = SPLIT_PATTERN.matcher(uniformPath); 085 int idx = -1; 086 while (matcher.find()) { 087 idx = matcher.start(); 088 } 089 if (idx == -1) { 090 return StringUtils.EMPTY_STRING; 091 } 092 return uniformPath.substring(0, idx); 093 } 094 095 /** Removes the first <code>count</code> segments from the given path. */ 096 public static String removeFirstSegments(String uniformPath, int count) { 097 String[] segments = splitPath(uniformPath); 098 return concatenate(Arrays.copyOfRange(segments, count, segments.length)); 099 } 100 101 /** Removes the last <code>count</code> segments from the given path. */ 102 public static String removeLastSegments(String uniformPath, int count) { 103 String[] segments = splitPath(uniformPath); 104 return concatenate(Arrays.copyOfRange(segments, 0, segments.length - count)); 105 } 106 107 /** Returns segments forming the given path. */ 108 public static String[] splitPath(String uniformPath) { 109 return SPLIT_PATTERN.split(uniformPath); 110 } 111 112 /** 113 * Returns the extension of the uniform path. 114 * 115 * @return File extension, i.e. "java" for "FileSystemUtils.java", or 116 * <code>null</code>, if the path has no extension (i.e. if a path 117 * contains no '.'), returns the empty string if the '.' is the path's 118 * last character. 119 */ 120 public static String getExtension(String uniformPath) { 121 String name = getElementName(uniformPath); 122 int posLastDot = name.lastIndexOf('.'); 123 if (posLastDot < 0) { 124 return null; 125 } 126 return name.substring(posLastDot + 1); 127 } 128 129 /** 130 * Replaces forward and backward slashes, not only system-specific separators, 131 * with a forward slash. We do this on purpose, since paths that e.g. are read 132 * from files do not necessarily contain the separators contained in 133 * File.separator. 134 */ 135 public static String normalizeAllSeparators(String path) { 136 return path.replaceAll("[/\\\\]+", "/"); 137 } 138 139 /** 140 * Creates a clean path by resolving duplicate slashes, single and double dots. 141 * This is the equivalent to path canonization on uniform paths. 142 */ 143 public static String cleanPath(String path) { 144 String[] parts = splitPath(path); 145 for (int i = 0; i < parts.length; ++i) { 146 // do not use StringUtils.isEmpty(), as we do not want trim 147 // semantics! 148 if (StringUtils.EMPTY_STRING.equals(parts[i]) || ".".equals(parts[i])) { 149 parts[i] = null; 150 } else if ("..".equals(parts[i])) { 151 // cancel last non-null (if any) 152 int j = i - 1; 153 for (; j >= 0; --j) { 154 if ("..".equals(parts[j])) { 155 // another '..' acts as boundary 156 break; 157 } 158 if (parts[j] != null) { 159 // cancel both parts of the path. 160 parts[j] = null; 161 parts[i] = null; 162 break; 163 } 164 } 165 } 166 } 167 168 return joinPath(parts); 169 } 170 171 /** Joins the given array as a path, but ignoring null entries. */ 172 private static String joinPath(String[] parts) { 173 StringBuilder sb = new StringBuilder(); 174 for (String part : parts) { 175 if (part != null) { 176 if (sb.length() > 0) { 177 sb.append(SEPARATOR_CHAR); 178 } 179 sb.append(part); 180 } 181 } 182 183 return sb.toString(); 184 } 185 186 /** 187 * For a uniform path denoting a file and a relative path, constructs the 188 * uniform path for the relatively addressed element. 189 */ 190 public static String resolveRelativePath(String basePath, String relative) { 191 // obtain "directory" from path denoting file 192 String directory = getParentPath(basePath); 193 if (!directory.isEmpty()) { 194 directory += SEPARATOR; 195 } 196 return cleanPath(directory + relative); 197 } 198 199 /** 200 * Returns the concatenated path of all given parts. Empty or null strings are 201 * ignored. 202 */ 203 public static String concatenate(String... parts) { 204 List<String> list = new ArrayList<String>(parts.length); 205 for (String part : parts) { 206 if (!StringUtils.isEmpty(part)) { 207 list.add(part); 208 } 209 } 210 return StringUtils.concat(list, SEPARATOR); 211 } 212 213 /** 214 * Creates a uniform path by prepending the project to the given path. If the 215 * project is null, it is ignored. 216 */ 217 public static String prependProject(String projectName, String path) { 218 return cleanPath(concatenate(projectName, path)); 219 } 220 221 /** 222 * Remove drive letters or unix root slash from path. Also normalizes the path. 223 */ 224 public static String createSystemIndependentPath(String path) { 225 Matcher m1 = DRIVE_LETTER_PATTERN.matcher(normalizeAllSeparators(path)); 226 if (m1.matches()) { 227 // Remove drive letter 228 path = m1.group(1); 229 } 230 231 // Remove unix root slash 232 path = StringUtils.stripPrefix(path, "/"); 233 return path; 234 } 235 236}