/*
* Copyright (c) 2002-2012 Alibaba Group Holding Limited.
* All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.citrus.util;
import static com.alibaba.citrus.util.StringUtil.*;
import java.io.File;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 用来处理文件路径和后缀的工具。
*
* @author Michael Zhou
*/
public class FileUtil {
// ==========================================================================
// 规格化路径。
// ==========================================================================
/**
* 规格化绝对路径。
* <p>
* 该方法返回以“<code>/</code>”开始的绝对路径。转换规则如下:
* </p>
* <ol>
* <li>路径为空,则返回<code>""</code>。</li>
* <li>将所有backslash("\\")转化成slash("/")。</li>
* <li>去除重复的"/"或"\\"。</li>
* <li>去除".",如果发现"..",则向上朔一级目录。</li>
* <li>保留路径末尾的"/"(如果有的话,除了空路径)。</li>
* <li>对于绝对路径,如果".."上朔的路径超过了根目录,则看作非法路径,抛出异常。</li>
* </ol>
*
* @param path 要规格化的路径
* @return 规格化后的路径
* @throws IllegalPathException 如果路径非法
*/
public static String normalizeAbsolutePath(String path) throws IllegalPathException {
return normalizePath(path, true, false, false);
}
/**
* 规格化绝对路径。
* <p>
* 该方法返回以“<code>/</code>”开始的绝对路径。转换规则如下:
* </p>
* <ol>
* <li>路径为空,则返回<code>""</code>。</li>
* <li>将所有backslash("\\")转化成slash("/")。</li>
* <li>去除重复的"/"或"\\"。</li>
* <li>去除".",如果发现"..",则向上朔一级目录。</li>
* <li>保留路径末尾的"/"(如果有的话,除了空路径和强制指定<code>removeTrailingSlash==true</code>)。</li>
* <li>对于绝对路径,如果".."上朔的路径超过了根目录,则看作非法路径,抛出异常。</li>
* </ol>
*
* @param path 要规格化的路径
* @param removeTrailingSlash 是否强制移除末尾的<code>"/"</code>
* @return 规格化后的路径
* @throws IllegalPathException 如果路径非法
*/
public static String normalizeAbsolutePath(String path, boolean removeTrailingSlash) throws IllegalPathException {
return normalizePath(path, true, false, removeTrailingSlash);
}
/**
* 规格化相对路径。
* <p>
* 该方法返回不以“<code>/</code>”开始的相对路径。转换规则如下:
* </p>
* <ol>
* <li>路径为空,则返回<code>""</code>。</li>
* <li>将所有backslash("\\")转化成slash("/")。</li>
* <li>去除重复的"/"或"\\"。</li>
* <li>去除".",如果发现"..",则向上朔一级目录。</li>
* <li>空相对路径返回""。</li>
* <li>保留路径末尾的"/"(如果有的话,除了空路径)。</li>
* </ol>
*
* @param path 要规格化的路径
* @return 规格化后的路径
* @throws IllegalPathException 如果路径非法
*/
public static String normalizeRelativePath(String path) throws IllegalPathException {
return normalizePath(path, false, true, false);
}
/**
* 规格化相对路径。
* <p>
* 该方法返回不以“<code>/</code>”开始的相对路径。转换规则如下:
* </p>
* <ol>
* <li>路径为空,则返回<code>""</code>。</li>
* <li>将所有backslash("\\")转化成slash("/")。</li>
* <li>去除重复的"/"或"\\"。</li>
* <li>去除".",如果发现"..",则向上朔一级目录。</li>
* <li>空相对路径返回""。</li>
* <li>保留路径末尾的"/"(如果有的话,除了空路径和强制指定<code>removeTrailingSlash==true</code>)。</li>
* </ol>
*
* @param path 要规格化的路径
* @param removeTrailingSlash 是否强制移除末尾的<code>"/"</code>
* @return 规格化后的路径
* @throws IllegalPathException 如果路径非法
*/
public static String normalizeRelativePath(String path, boolean removeTrailingSlash) throws IllegalPathException {
return normalizePath(path, false, true, removeTrailingSlash);
}
/**
* 规格化路径。规则如下:
* <ol>
* <li>路径为空,则返回<code>""</code>。</li>
* <li>将所有backslash("\\")转化成slash("/")。</li>
* <li>去除重复的"/"或"\\"。</li>
* <li>去除".",如果发现"..",则向上朔一级目录。</li>
* <li>空绝对路径返回"/",空相对路径返回""。</li>
* <li>保留路径末尾的"/"(如果有的话,除了空路径)。</li>
* <li>对于绝对路径,如果".."上朔的路径超过了根目录,则看作非法路径,抛出异常。</li>
* </ol>
*
* @param path 要规格化的路径
* @return 规格化后的路径
* @throws IllegalPathException 如果路径非法
*/
public static String normalizePath(String path) throws IllegalPathException {
return normalizePath(path, false, false, false);
}
/**
* 规格化路径。规则如下:
* <ol>
* <li>路径为空,则返回<code>""</code>。</li>
* <li>将所有backslash("\\")转化成slash("/")。</li>
* <li>去除重复的"/"或"\\"。</li>
* <li>去除".",如果发现"..",则向上朔一级目录。</li>
* <li>空绝对路径返回"/",空相对路径返回""。</li>
* <li>保留路径末尾的"/"(如果有的话,除了空路径和强制指定<code>removeTrailingSlash==true</code>)。</li>
* <li>对于绝对路径,如果".."上朔的路径超过了根目录,则看作非法路径,抛出异常。</li>
* </ol>
*
* @param path 要规格化的路径
* @param removeTrailingSlash 是否强制移除末尾的<code>"/"</code>
* @return 规格化后的路径
* @throws IllegalPathException 如果路径非法
*/
public static String normalizePath(String path, boolean removeTrailingSlash) throws IllegalPathException {
return normalizePath(path, false, false, removeTrailingSlash);
}
private static String normalizePath(String path, boolean forceAbsolute, boolean forceRelative,
boolean removeTrailingSlash) throws IllegalPathException {
char[] pathChars = trimToEmpty(path).toCharArray();
int length = pathChars.length;
// 检查绝对路径,以及path尾部的"/"
boolean startsWithSlash = false;
boolean endsWithSlash = false;
if (length > 0) {
char firstChar = pathChars[0];
char lastChar = pathChars[length - 1];
startsWithSlash = firstChar == '/' || firstChar == '\\';
endsWithSlash = lastChar == '/' || lastChar == '\\';
}
StringBuilder buf = new StringBuilder(length);
boolean isAbsolutePath = forceAbsolute || !forceRelative && startsWithSlash;
int index = startsWithSlash ? 0 : -1;
int level = 0;
if (isAbsolutePath) {
buf.append("/");
}
while (index < length) {
// 跳到第一个非slash字符,或末尾
index = indexOfSlash(pathChars, index + 1, false);
if (index == length) {
break;
}
// 取得下一个slash index,或末尾
int nextSlashIndex = indexOfSlash(pathChars, index, true);
String element = new String(pathChars, index, nextSlashIndex - index);
index = nextSlashIndex;
// 忽略"."
if (".".equals(element)) {
continue;
}
// 回朔".."
if ("..".equals(element)) {
if (level == 0) {
// 如果是绝对路径,../试图越过最上层目录,这是不可能的,
// 抛出路径非法的异常。
if (isAbsolutePath) {
throw new IllegalPathException(path);
} else {
buf.append("../");
}
} else {
buf.setLength(pathChars[--level]);
}
continue;
}
// 添加到path
pathChars[level++] = (char) buf.length(); // 将已经读过的chars空间用于记录指定level的index
buf.append(element).append('/');
}
// 除去最后的"/"
if (buf.length() > 0) {
if (!endsWithSlash || removeTrailingSlash) {
buf.setLength(buf.length() - 1);
}
}
return buf.toString();
}
private static int indexOfSlash(char[] chars, int beginIndex, boolean slash) {
int i = beginIndex;
for (; i < chars.length; i++) {
char ch = chars[i];
if (slash) {
if (ch == '/' || ch == '\\') {
break; // if a slash
}
} else {
if (ch != '/' && ch != '\\') {
break; // if not a slash
}
}
}
return i;
}
// ==========================================================================
// 取得基于指定basedir规格化路径。
// ==========================================================================
/**
* 如果指定路径已经是绝对路径,则规格化后直接返回之,否则取得基于指定basedir的规格化路径。
*
* @param basedir 根目录,如果<code>path</code>为相对路径,表示基于此目录
* @param path 要检查的路径
* @return 规格化的绝对路径
* @throws IllegalPathException 如果路径非法
*/
public static String getAbsolutePathBasedOn(String basedir, String path) throws IllegalPathException {
// 如果path为绝对路径,则规格化后返回
boolean isAbsolutePath = false;
path = trimToEmpty(path);
if (path.length() > 0) {
char firstChar = path.charAt(0);
isAbsolutePath = firstChar == '/' || firstChar == '\\';
}
if (!isAbsolutePath) {
// 如果path为相对路径,将它和basedir合并。
if (path.length() > 0) {
path = trimToEmpty(basedir) + "/" + path;
} else {
path = trimToEmpty(basedir);
}
}
return normalizeAbsolutePath(path);
}
/**
* 取得和系统相关的绝对路径。
*
* @throws IllegalPathException 如果basedir不是绝对路径
*/
public static String getSystemDependentAbsolutePathBasedOn(String basedir, String path) {
path = trimToEmpty(path);
boolean endsWithSlash = path.endsWith("/") || path.endsWith("\\");
File pathFile = new File(path);
if (pathFile.isAbsolute()) {
// 如果path已经是绝对路径了,则直接返回之。
path = pathFile.getAbsolutePath();
} else {
// 否则以basedir为基本路径。
// 下面确保basedir本身为绝对路径。
basedir = trimToEmpty(basedir);
File baseFile = new File(basedir);
if (baseFile.isAbsolute()) {
path = new File(baseFile, path).getAbsolutePath();
} else {
throw new IllegalPathException("Basedir is not absolute path: " + basedir);
}
}
if (endsWithSlash) {
path = path + '/';
}
return normalizePath(path);
}
// ==========================================================================
// 取得相对于指定basedir相对路径。
// ==========================================================================
/**
* 取得相对于指定根目录的相对路径。
*
* @param basedir 根目录
* @param path 要计算的路径
* @return 如果<code>path</code>和<code>basedir</code>是兼容的,则返回相对于
* <code>basedir</code>的相对路径,否则返回<code>path</code>本身。
* @throws IllegalPathException 如果路径非法
*/
public static String getRelativePath(String basedir, String path) throws IllegalPathException {
// 取得规格化的basedir,确保其为绝对路径
basedir = normalizeAbsolutePath(basedir);
// 取得规格化的path
path = getAbsolutePathBasedOn(basedir, path);
// 保留path尾部的"/"
boolean endsWithSlash = path.endsWith("/");
// 按"/"分隔basedir和path
String[] baseParts = StringUtil.split(basedir, '/');
String[] parts = StringUtil.split(path, '/');
StringBuilder buf = new StringBuilder();
int i = 0;
while (i < baseParts.length && i < parts.length && baseParts[i].equals(parts[i])) {
i++;
}
if (i < baseParts.length && i < parts.length) {
for (int j = i; j < baseParts.length; j++) {
buf.append("..").append('/');
}
}
for (; i < parts.length; i++) {
buf.append(parts[i]);
if (i < parts.length - 1) {
buf.append('/');
}
}
if (endsWithSlash && buf.length() > 0 && buf.charAt(buf.length() - 1) != '/') {
buf.append('/');
}
return buf.toString();
}
// ==========================================================================
// 取得文件名后缀。
// ==========================================================================
/**
* 取得文件路径的后缀。
* <ul>
* <li>未指定文件名 - 返回<code>null</code>。</li>
* <li>文件名没有后缀 - 返回<code>null</code>。</li>
* </ul>
*/
public static String getExtension(String fileName) {
return getExtension(fileName, null, false);
}
/**
* 取得文件路径的后缀。
* <ul>
* <li>未指定文件名 - 返回<code>null</code>。</li>
* <li>文件名没有后缀 - 返回<code>null</code>。</li>
* </ul>
*/
public static String getExtension(String fileName, boolean toLowerCase) {
return getExtension(fileName, null, toLowerCase);
}
/**
* 取得文件路径的后缀。
* <ul>
* <li>未指定文件名 - 返回<code>null</code>。</li>
* <li>文件名没有后缀 - 返回指定字符串<code>nullExt</code>。</li>
* </ul>
*/
public static String getExtension(String fileName, String nullExt) {
return getExtension(fileName, nullExt, false);
}
/**
* 取得文件路径的后缀。
* <ul>
* <li>未指定文件名 - 返回<code>null</code>。</li>
* <li>文件名没有后缀 - 返回指定字符串<code>nullExt</code>。</li>
* </ul>
*/
public static String getExtension(String fileName, String nullExt, boolean toLowerCase) {
fileName = trimToNull(fileName);
if (fileName == null) {
return null;
}
fileName = fileName.replace('\\', '/');
fileName = fileName.substring(fileName.lastIndexOf("/") + 1);
int index = fileName.lastIndexOf(".");
String ext = null;
if (index >= 0) {
ext = trimToNull(fileName.substring(index + 1));
}
if (ext == null) {
return nullExt;
} else {
return toLowerCase ? ext.toLowerCase() : ext;
}
}
/**
* 取得指定路径的名称和后缀。
*
* @param path 路径
* @return 路径和后缀
*/
public static FileNameAndExtension getFileNameAndExtension(String path) {
return getFileNameAndExtension(path, false);
}
/**
* 取得指定路径的名称和后缀。
*
* @param path 路径
* @return 路径和后缀
*/
public static FileNameAndExtension getFileNameAndExtension(String path, boolean extensionToLowerCase) {
path = StringUtil.trimToEmpty(path);
String fileName = path;
String extension = null;
if (!StringUtil.isEmpty(path)) {
// 如果找到后缀,则index >= 0,且extension != null(除非name以.结尾)
int index = path.lastIndexOf('.');
if (index >= 0) {
extension = StringUtil.trimToNull(StringUtil.substring(path, index + 1));
if (!StringUtil.containsNone(extension, "/\\")) {
extension = null;
index = -1;
}
}
if (index >= 0) {
fileName = StringUtil.substring(path, 0, index);
}
}
return new FileNameAndExtension(fileName, extension, extensionToLowerCase);
}
/**
* 规格化文件名后缀。
* <ul>
* <li>除去两边空白。</li>
* <li>转成小写。</li>
* <li>除去开头的“<code>.</code>”。</li>
* <li>对空白的后缀,返回<code>null</code>。</li>
* </ul>
*/
public static String normalizeExtension(String ext) {
ext = trimToNull(ext);
if (ext != null) {
ext = ext.toLowerCase();
if (ext.startsWith(".")) {
ext = trimToNull(ext.substring(1));
}
}
return ext;
}
private static final Pattern schemePrefixPattern = Pattern.compile(
"(file:/*[a-z]:)|(\\w+://.+?/)|((jar|zip):.+!/)|(\\w+:)", Pattern.CASE_INSENSITIVE);
/**
* 根据指定url和相对路径,计算出相对路径所对应的完整url。类似于<code>URI.resolve()</code>
* 方法,然后后者不能正确处理jar类型的URL。
*/
public static String resolve(String url, String relativePath) {
url = trimToEmpty(url);
Matcher m = schemePrefixPattern.matcher(url);
int index = 0;
if (m.find()) {
index = m.end();
if (url.charAt(index - 1) == '/') {
index--;
}
}
return url.substring(0, index) + normalizeAbsolutePath(url.substring(index) + "/../" + relativePath);
}
public static class FileNameAndExtension {
private final String fileName;
private final String extension;
private FileNameAndExtension(String fileName, String extension, boolean extensionToLowerCase) {
this.fileName = fileName;
this.extension = extensionToLowerCase ? toLowerCase(extension) : extension;
}
public String getFileName() {
return fileName;
}
public String getExtension() {
return extension;
}
@Override
public String toString() {
return extension == null ? fileName : fileName + "." + extension;
}
}
}