blob: fe29e8a5515b15746ef4a52753fd978ab2db7189 [file] [log] [blame]
/*
* Copyright (C) 2010 The Android Open Source Project
*
* 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.android.sdklib.io;
import com.android.SdkConstants;
import com.android.annotations.NonNull;
import com.google.common.io.Closer;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Properties;
import java.util.regex.Pattern;
/**
* Wraps some common {@link File} operations on files and folders.
* <p/>
* This makes it possible to override/mock/stub some file operations in unit tests.
*/
public class FileOp implements IFileOp {
public static final File[] EMPTY_FILE_ARRAY = new File[0];
/**
* Reflection method for File.setExecutable(boolean, boolean). Only present in Java 6.
*/
private static Method sFileSetExecutable = null;
/**
* Parameters to call File.setExecutable through reflection.
*/
private static final Object[] sFileSetExecutableParams = new Object[] {
Boolean.TRUE, Boolean.FALSE };
// static initialization of sFileSetExecutable.
static {
try {
sFileSetExecutable = File.class.getMethod("setExecutable", //$NON-NLS-1$
boolean.class, boolean.class);
} catch (SecurityException e) {
// do nothing we'll use chmod instead
} catch (NoSuchMethodException e) {
// do nothing we'll use chmod instead
}
}
/**
* Appends the given {@code segments} to the {@code base} file.
*
* @param base A base file, non-null.
* @param segments Individual folder or filename segments to append to the base file.
* @return A new file representing the concatenation of the base path with all the segments.
*/
public static File append(@NonNull File base, @NonNull String...segments) {
for (String segment : segments) {
base = new File(base, segment);
}
return base;
}
/**
* Appends the given {@code segments} to the {@code base} file.
*
* @param base A base file path, non-empty and non-null.
* @param segments Individual folder or filename segments to append to the base path.
* @return A new file representing the concatenation of the base path with all the segments.
*/
public static File append(@NonNull String base, @NonNull String...segments) {
return append(new File(base), segments);
}
/**
* Helper to delete a file or a directory.
* For a directory, recursively deletes all of its content.
* Files that cannot be deleted right away are marked for deletion on exit.
* It's ok for the file or folder to not exist at all.
* The argument can be null.
*/
@Override
public void deleteFileOrFolder(@NonNull File fileOrFolder) {
if (fileOrFolder != null) {
if (isDirectory(fileOrFolder)) {
// Must delete content recursively first
File[] files = fileOrFolder.listFiles();
if (files != null) {
for (File item : files) {
deleteFileOrFolder(item);
}
}
}
// Don't try to delete it if it doesn't exist.
if (!exists(fileOrFolder)) {
return;
}
if (SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_WINDOWS) {
// Trying to delete a resource on windows might fail if there's a file
// indexer locking the resource. Generally retrying will be enough to
// make it work.
//
// Try for half a second before giving up.
for (int i = 0; i < 5; i++) {
if (fileOrFolder.delete()) {
return;
}
try {
Thread.sleep(100 /*ms*/);
} catch (InterruptedException e) {
// Ignore.
}
}
fileOrFolder.deleteOnExit();
} else {
// On Linux or Mac, just straight deleting it should just work.
if (!fileOrFolder.delete()) {
fileOrFolder.deleteOnExit();
}
}
}
}
/**
* Sets the executable Unix permission (+x) on a file or folder.
* <p/>
* This attempts to use File#setExecutable through reflection if
* it's available.
* If this is not available, this invokes a chmod exec instead,
* so there is no guarantee of it being fast.
* <p/>
* Caller must make sure to not invoke this under Windows.
*
* @param file The file to set permissions on.
* @throws IOException If an I/O error occurs
*/
@Override
public void setExecutablePermission(@NonNull File file) throws IOException {
if (sFileSetExecutable != null) {
try {
sFileSetExecutable.invoke(file, sFileSetExecutableParams);
return;
} catch (IllegalArgumentException e) {
// we'll run chmod below
} catch (IllegalAccessException e) {
// we'll run chmod below
} catch (InvocationTargetException e) {
// we'll run chmod below
}
}
Runtime.getRuntime().exec(new String[] {
"chmod", "+x", file.getAbsolutePath() //$NON-NLS-1$ //$NON-NLS-2$
});
}
@Override
public void setReadOnly(@NonNull File file) {
file.setReadOnly();
}
/**
* Copies a binary file.
*
* @param source the source file to copy.
* @param dest the destination file to write.
* @throws FileNotFoundException if the source file doesn't exist.
* @throws IOException if there's a problem reading or writing the file.
*/
@Override
public void copyFile(@NonNull File source, @NonNull File dest) throws IOException {
byte[] buffer = new byte[8192];
FileInputStream fis = null;
FileOutputStream fos = null;
try {
fis = new FileInputStream(source);
fos = new FileOutputStream(dest);
int read;
while ((read = fis.read(buffer)) != -1) {
fos.write(buffer, 0, read);
}
} finally {
if (fis != null) {
try {
fis.close();
} catch (IOException e) {
// Ignore.
}
}
if (fos != null) {
try {
fos.close();
} catch (IOException e) {
// Ignore.
}
}
}
}
/**
* Checks whether 2 binary files are the same.
*
* @param file1 the source file to copy
* @param file2 the destination file to write
* @throws FileNotFoundException if the source files don't exist.
* @throws IOException if there's a problem reading the files.
*/
@Override
public boolean isSameFile(@NonNull File file1, @NonNull File file2) throws IOException {
if (file1.length() != file2.length()) {
return false;
}
FileInputStream fis1 = null;
FileInputStream fis2 = null;
try {
fis1 = new FileInputStream(file1);
fis2 = new FileInputStream(file2);
byte[] buffer1 = new byte[8192];
byte[] buffer2 = new byte[8192];
int read1;
while ((read1 = fis1.read(buffer1)) != -1) {
int read2 = 0;
while (read2 < read1) {
int n = fis2.read(buffer2, read2, read1 - read2);
if (n == -1) {
break;
}
}
if (read2 != read1) {
return false;
}
if (!Arrays.equals(buffer1, buffer2)) {
return false;
}
}
} finally {
if (fis2 != null) {
try {
fis2.close();
} catch (IOException e) {
// ignore
}
}
if (fis1 != null) {
try {
fis1.close();
} catch (IOException e) {
// ignore
}
}
}
return true;
}
/** Invokes {@link File#isFile()} on the given {@code file}. */
@Override
public boolean isFile(@NonNull File file) {
return file.isFile();
}
/** Invokes {@link File#isDirectory()} on the given {@code file}. */
@Override
public boolean isDirectory(@NonNull File file) {
return file.isDirectory();
}
/** Invokes {@link File#exists()} on the given {@code file}. */
@Override
public boolean exists(@NonNull File file) {
return file.exists();
}
/** Invokes {@link File#length()} on the given {@code file}. */
@Override
public long length(@NonNull File file) {
return file.length();
}
/**
* Invokes {@link File#delete()} on the given {@code file}.
* Note: for a recursive folder version, consider {@link #deleteFileOrFolder(File)}.
*/
@Override
public boolean delete(@NonNull File file) {
return file.delete();
}
/** Invokes {@link File#mkdirs()} on the given {@code file}. */
@Override
public boolean mkdirs(@NonNull File file) {
return file.mkdirs();
}
/**
* Invokes {@link File#listFiles()} on the given {@code file}.
* Contrary to the Java API, this returns an empty array instead of null when the
* directory does not exist.
*/
@Override
@NonNull
public File[] listFiles(@NonNull File file) {
File[] r = file.listFiles();
if (r == null) {
return EMPTY_FILE_ARRAY;
} else {
return r;
}
}
/** Invokes {@link File#renameTo(File)} on the given files. */
@Override
public boolean renameTo(@NonNull File oldFile, @NonNull File newFile) {
return oldFile.renameTo(newFile);
}
/** Creates a new {@link OutputStream} for the given {@code file}. */
@Override
@NonNull
public OutputStream newFileOutputStream(@NonNull File file) throws FileNotFoundException {
return new FileOutputStream(file);
}
/** Creates a new {@link InputStream} for the given {@code file}. */
@Override
@NonNull
public InputStream newFileInputStream(@NonNull File file) throws FileNotFoundException {
return new FileInputStream(file);
}
@Override
@NonNull
public Properties loadProperties(@NonNull File file) {
Properties props = new Properties();
Closer closer = Closer.create();
try {
FileInputStream fis = closer.register(new FileInputStream(file));
props.load(fis);
} catch (IOException ignore) {
} finally {
try {
closer.close();
} catch (IOException e) {
}
}
return props;
}
@Override
public void saveProperties(
@NonNull File file,
@NonNull Properties props,
@NonNull String comments) throws IOException {
Closer closer = Closer.create();
try {
OutputStream fos = closer.register(newFileOutputStream(file));
props.store(fos, comments);
} catch (Throwable e) {
throw closer.rethrow(e);
} finally {
closer.close();
}
}
@Override
public long lastModified(@NonNull File file) {
return file.lastModified();
}
/**
* Computes a relative path from "toBeRelative" relative to "baseDir".
*
* Rule:
* - let relative2 = makeRelative(path1, path2)
* - then pathJoin(path1 + relative2) == path2 after canonicalization.
*
* Principle:
* - let base = /c1/c2.../cN/a1/a2../aN
* - let toBeRelative = /c1/c2.../cN/b1/b2../bN
* - result is removes the common paths, goes back from aN to cN then to bN:
* - result = ../..../../1/b2../bN
*
* @param baseDir The base directory to be relative to.
* @param toBeRelative The file or directory to make relative to the base.
* @return A path that makes toBeRelative relative to baseDir.
* @throws IOException If drive letters don't match on Windows or path canonicalization fails.
*/
@NonNull
public static String makeRelative(@NonNull File baseDir, @NonNull File toBeRelative)
throws IOException {
return makeRelativeImpl(
baseDir.getCanonicalPath(),
toBeRelative.getCanonicalPath(),
SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_WINDOWS,
File.separator);
}
/**
* Implementation detail of makeRelative to make it testable
* Independently of the platform.
*/
@NonNull
static String makeRelativeImpl(@NonNull String path1,
@NonNull String path2,
boolean isWindows,
@NonNull String dirSeparator)
throws IOException {
if (isWindows) {
// Check whether both path are on the same drive letter, if any.
String p1 = path1;
String p2 = path2;
char drive1 = (p1.length() >= 2 && p1.charAt(1) == ':') ? p1.charAt(0) : 0;
char drive2 = (p2.length() >= 2 && p2.charAt(1) == ':') ? p2.charAt(0) : 0;
if (drive1 != drive2) {
// Either a mix of UNC vs drive or not the same drives.
throw new IOException("makeRelative: incompatible drive letters");
}
}
String[] segments1 = path1.split(Pattern.quote(dirSeparator));
String[] segments2 = path2.split(Pattern.quote(dirSeparator));
int len1 = segments1.length;
int len2 = segments2.length;
int len = Math.min(len1, len2);
int start = 0;
for (; start < len; start++) {
// On Windows should compare in case-insensitive.
// Mac & Linux file systems can be both type, although their default
// is generally to have a case-sensitive file system.
if (( isWindows && !segments1[start].equalsIgnoreCase(segments2[start])) ||
(!isWindows && !segments1[start].equals(segments2[start]))) {
break;
}
}
StringBuilder result = new StringBuilder();
for (int i = start; i < len1; i++) {
result.append("..").append(dirSeparator);
}
while (start < len2) {
result.append(segments2[start]);
if (++start < len2) {
result.append(dirSeparator);
}
}
return result.toString();
}
}