| /* |
| * Copyright (C) 2011 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.android.annotations.Nullable; |
| import com.google.common.base.Charsets; |
| import com.google.common.collect.Maps; |
| import com.google.common.collect.Sets; |
| |
| import java.io.ByteArrayInputStream; |
| import java.io.ByteArrayOutputStream; |
| import java.io.File; |
| import java.io.FileInputStream; |
| import java.io.FileNotFoundException; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.OutputStream; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Map.Entry; |
| import java.util.Properties; |
| import java.util.Set; |
| import java.util.TreeSet; |
| import java.util.regex.Matcher; |
| import java.util.regex.Pattern; |
| |
| |
| /** |
| * Mock version of {@link FileOp} that wraps some common {@link File} |
| * operations on files and folders. |
| * <p/> |
| * This version does not perform any file operation. Instead it records a textual |
| * representation of all the file operations performed. |
| * <p/> |
| * To avoid cross-platform path issues (e.g. Windows path), the methods here should |
| * always use rooted (aka absolute) unix-looking paths, e.g. "/dir1/dir2/file3". |
| * When processing {@link File}, you can convert them using {@link #getAgnosticAbsPath(File)}. |
| */ |
| public class MockFileOp implements IFileOp { |
| |
| private final Map<String, FileInfo> mExistingFiles = Maps.newTreeMap(); |
| private final Set<String> mExistingFolders = Sets.newTreeSet(); |
| private final List<StringOutputStream> mOutputStreams = new ArrayList<StringOutputStream>(); |
| |
| public MockFileOp() { |
| } |
| |
| /** Resets the internal state, as if the object had been newly created. */ |
| public void reset() { |
| mExistingFiles.clear(); |
| mExistingFolders.clear(); |
| mOutputStreams.clear(); |
| } |
| |
| @NonNull |
| public String getAgnosticAbsPath(@NonNull File file) { |
| return getAgnosticAbsPath(file.getAbsolutePath()); |
| } |
| |
| @NonNull |
| public String getAgnosticAbsPath(@NonNull String path) { |
| if (SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_WINDOWS) { |
| // Try to convert the windows-looking path to a unix-looking one |
| path = path.replace('\\', '/'); |
| path = path.replaceAll("^[A-Z]:", ""); //$NON-NLS-1$ //$NON-NLS-2$ |
| } |
| return path; |
| } |
| |
| /** |
| * Records a new absolute file path. |
| * Parent folders are not automatically created. |
| */ |
| public void recordExistingFile(@NonNull File file) { |
| recordExistingFile(getAgnosticAbsPath(file), 0, (byte[])null); |
| } |
| |
| /** |
| * Records a new absolute file path. |
| * Parent folders are not automatically created. |
| * <p/> |
| * The syntax should always look "unix-like", e.g. "/dir/file". |
| * On Windows that means you'll want to use {@link #getAgnosticAbsPath(File)}. |
| * @param absFilePath A unix-like file path, e.g. "/dir/file" |
| */ |
| public void recordExistingFile(@NonNull String absFilePath) { |
| recordExistingFile(absFilePath, 0, (byte[])null); |
| } |
| |
| /** |
| * Records a new absolute file path & its input stream content. |
| * Parent folders are not automatically created. |
| * <p/> |
| * The syntax should always look "unix-like", e.g. "/dir/file". |
| * On Windows that means you'll want to use {@link #getAgnosticAbsPath(File)}. |
| * @param absFilePath A unix-like file path, e.g. "/dir/file" |
| * @param inputStream A non-null byte array of content to return |
| * via {@link #newFileInputStream(File)}. |
| */ |
| public void recordExistingFile(@NonNull String absFilePath, @Nullable byte[] inputStream) { |
| recordExistingFile(absFilePath, 0, inputStream); |
| } |
| |
| /** |
| * Records a new absolute file path & its input stream content. |
| * Parent folders are not automatically created. |
| * <p/> |
| * The syntax should always look "unix-like", e.g. "/dir/file". |
| * On Windows that means you'll want to use {@link #getAgnosticAbsPath(File)}. |
| * @param absFilePath A unix-like file path, e.g. "/dir/file" |
| * @param content A non-null UTF-8 content string to return |
| * via {@link #newFileInputStream(File)}. |
| */ |
| public void recordExistingFile(@NonNull String absFilePath, @NonNull String content) { |
| recordExistingFile(absFilePath, 0, content.getBytes(Charsets.UTF_8)); |
| } |
| |
| /** |
| * Records a new absolute file path & its input stream content. |
| * Parent folders are not automatically created. |
| * <p/> |
| * The syntax should always look "unix-like", e.g. "/dir/file". |
| * On Windows that means you'll want to use {@link #getAgnosticAbsPath(File)}. |
| * @param absFilePath A unix-like file path, e.g. "/dir/file" |
| * @param inputStream A non-null byte array of content to return |
| * via {@link #newFileInputStream(File)}. |
| */ |
| public void recordExistingFile(@NonNull String absFilePath, |
| long lastModified, |
| @Nullable byte[] inputStream) { |
| mExistingFiles.put(absFilePath, new FileInfo(lastModified, inputStream)); |
| } |
| |
| /** |
| * Records a new absolute file path & its input stream content. |
| * Parent folders are not automatically created. |
| * <p/> |
| * The syntax should always look "unix-like", e.g. "/dir/file". |
| * On Windows that means you'll want to use {@link #getAgnosticAbsPath(File)}. |
| * @param absFilePath A unix-like file path, e.g. "/dir/file" |
| * @param content A non-null UTF-8 content string to return |
| * via {@link #newFileInputStream(File)}. |
| */ |
| public void recordExistingFile(@NonNull String absFilePath, |
| long lastModified, |
| @NonNull String content) { |
| recordExistingFile(absFilePath, lastModified, content.getBytes(Charsets.UTF_8)); |
| } |
| |
| /** |
| * Records a new absolute folder path. |
| * Parent folders are not automatically created. |
| */ |
| public void recordExistingFolder(File folder) { |
| mExistingFolders.add(getAgnosticAbsPath(folder)); |
| } |
| |
| /** |
| * Records a new absolute folder path. |
| * Parent folders are not automatically created. |
| * <p/> |
| * The syntax should always look "unix-like", e.g. "/dir/file". |
| * On Windows that means you'll want to use {@link #getAgnosticAbsPath(File)}. |
| * @param absFolderPath A unix-like folder path, e.g. "/dir/file" |
| */ |
| public void recordExistingFolder(String absFolderPath) { |
| mExistingFolders.add(absFolderPath); |
| } |
| |
| /** |
| * Returns true if a file with the given path has been recorded. |
| */ |
| public boolean hasRecordedExistingFile(File file) { |
| return mExistingFiles.containsKey(getAgnosticAbsPath(file)); |
| } |
| |
| /** |
| * Returns true if a folder with the given path has been recorded. |
| */ |
| public boolean hasRecordedExistingFolder(File folder) { |
| return mExistingFolders.contains(getAgnosticAbsPath(folder)); |
| } |
| |
| /** |
| * Returns the list of paths added using {@link #recordExistingFile(String)} |
| * and eventually updated by {@link #delete(File)} operations. |
| * <p/> |
| * The returned list is sorted by alphabetic absolute path string. |
| */ |
| @NonNull |
| public String[] getExistingFiles() { |
| Set<String> files = mExistingFiles.keySet(); |
| return files.toArray(new String[files.size()]); |
| } |
| |
| /** |
| * Returns the list of folder paths added using {@link #recordExistingFolder(String)} |
| * and eventually updated {@link #delete(File)} or {@link #mkdirs(File)} operations. |
| * <p/> |
| * The returned list is sorted by alphabetic absolute path string. |
| */ |
| @NonNull |
| public String[] getExistingFolders() { |
| return mExistingFolders.toArray(new String[mExistingFolders.size()]); |
| } |
| |
| /** |
| * Returns the {@link StringOutputStream#toString()} as an array, in creation order. |
| * Array can be empty but not null. |
| */ |
| @NonNull |
| public String[] getOutputStreams() { |
| int n = mOutputStreams.size(); |
| String[] result = new String[n]; |
| for (int i = 0; i < n; i++) { |
| result[i] = mOutputStreams.get(i).toString(); |
| } |
| return result; |
| } |
| |
| /** |
| * 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. |
| * The argument can be null. |
| */ |
| @Override |
| public void deleteFileOrFolder(@NonNull File fileOrFolder) { |
| if (fileOrFolder != null) { |
| if (isDirectory(fileOrFolder)) { |
| // Must delete content recursively first |
| for (File item : listFiles(fileOrFolder)) { |
| deleteFileOrFolder(item); |
| } |
| } |
| delete(fileOrFolder); |
| } |
| } |
| |
| /** |
| * {@inheritDoc} |
| * <p/> |
| * <em>Note: this mock version does nothing.</em> |
| */ |
| @Override |
| public void setExecutablePermission(@NonNull File file) throws IOException { |
| // pass |
| } |
| |
| /** |
| * {@inheritDoc} |
| * <p/> |
| * <em>Note: this mock version does nothing.</em> |
| */ |
| @Override |
| public void setReadOnly(@NonNull File file) { |
| // pass |
| } |
| |
| /** |
| * {@inheritDoc} |
| * <p/> |
| * <em>Note: this mock version does nothing.</em> |
| */ |
| @Override |
| public void copyFile(@NonNull File source, @NonNull File dest) throws IOException { |
| // pass |
| throw new UnsupportedOperationException("MockFileUtils.copyFile is not supported."); //$NON-NLS-1$ |
| } |
| |
| /** |
| * 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 { |
| String path1 = getAgnosticAbsPath(file1); |
| String path2 = getAgnosticAbsPath(file2); |
| FileInfo fi1 = mExistingFiles.get(path1); |
| FileInfo fi2 = mExistingFiles.get(path2); |
| |
| if (fi1 == null) { |
| throw new FileNotFoundException("[isSameFile] Mock file not defined: " + path1); |
| } |
| |
| if (fi1 == fi2) { |
| return true; |
| } |
| |
| if (fi2 == null) { |
| throw new FileNotFoundException("[isSameFile] Mock file not defined: " + path2); |
| } |
| |
| byte[] content1 = fi1.getContent(); |
| byte[] content2 = fi2.getContent(); |
| |
| if (content1 == null) { |
| throw new IOException("[isSameFile] Mock file has no content: " + path1); |
| } |
| if (content2 == null) { |
| throw new IOException("[isSameFile] Mock file has no content: " + path2); |
| } |
| |
| return Arrays.equals(content1, content2); |
| } |
| |
| /** Invokes {@link File#isFile()} on the given {@code file}. */ |
| @Override |
| public boolean isFile(@NonNull File file) { |
| String path = getAgnosticAbsPath(file); |
| return mExistingFiles.containsKey(path); |
| } |
| |
| /** Invokes {@link File#isDirectory()} on the given {@code file}. */ |
| @Override |
| public boolean isDirectory(@NonNull File file) { |
| String path = getAgnosticAbsPath(file); |
| if (mExistingFolders.contains(path)) { |
| return true; |
| } |
| |
| // If we defined a file or folder as a child of the requested file path, |
| // then the directory exists implicitely. |
| Pattern pathRE = Pattern.compile( |
| Pattern.quote(path + (path.endsWith("/") ? "" : '/')) + //$NON-NLS-1$ //$NON-NLS-2$ |
| ".*"); //$NON-NLS-1$ |
| |
| for (String folder : mExistingFolders) { |
| if (pathRE.matcher(folder).matches()) { |
| return true; |
| } |
| } |
| for (String filePath : mExistingFiles.keySet()) { |
| if (pathRE.matcher(filePath).matches()) { |
| return true; |
| } |
| } |
| |
| return false; |
| } |
| |
| /** Invokes {@link File#exists()} on the given {@code file}. */ |
| @Override |
| public boolean exists(@NonNull File file) { |
| return isFile(file) || isDirectory(file); |
| } |
| |
| /** Invokes {@link File#length()} on the given {@code file}. */ |
| @Override |
| public long length(@NonNull File file) { |
| throw new UnsupportedOperationException("MockFileUtils.length is not supported."); //$NON-NLS-1$ |
| } |
| |
| @Override |
| public boolean delete(@NonNull File file) { |
| String path = getAgnosticAbsPath(file); |
| |
| if (mExistingFiles.remove(path) != null) { |
| return true; |
| } |
| |
| boolean hasSubfiles = false; |
| for (String folder : mExistingFolders) { |
| if (folder.startsWith(path) && !folder.equals(path)) { |
| // the File.delete operation is not recursive and would fail to remove |
| // a root dir that is not empty. |
| return false; |
| } |
| } |
| if (!hasSubfiles) { |
| for (String filePath : mExistingFiles.keySet()) { |
| if (filePath.startsWith(path) && !filePath.equals(path)) { |
| // the File.delete operation is not recursive and would fail to remove |
| // a root dir that is not empty. |
| return false; |
| } |
| } |
| } |
| |
| return mExistingFolders.remove(path); |
| } |
| |
| /** Invokes {@link File#mkdirs()} on the given {@code file}. */ |
| @Override |
| public boolean mkdirs(@NonNull File file) { |
| for (; file != null; file = file.getParentFile()) { |
| String path = getAgnosticAbsPath(file); |
| mExistingFolders.add(path); |
| } |
| return true; |
| } |
| |
| /** |
| * Invokes {@link File#listFiles()} on the given {@code file}. |
| * The returned list is sorted by alphabetic absolute path string. |
| * Might return an empty array but never null. |
| */ |
| @NonNull |
| @Override |
| public File[] listFiles(@NonNull File file) { |
| TreeSet<File> files = new TreeSet<File>(); |
| |
| String path = getAgnosticAbsPath(file); |
| Pattern pathRE = Pattern.compile( |
| Pattern.quote(path + (path.endsWith("/") ? "" : '/')) + //$NON-NLS-1$ //$NON-NLS-2$ |
| ".*"); //$NON-NLS-1$ |
| |
| for (String folder : mExistingFolders) { |
| if (pathRE.matcher(folder).matches()) { |
| files.add(new File(folder)); |
| } |
| } |
| for (String filePath : mExistingFiles.keySet()) { |
| if (pathRE.matcher(filePath).matches()) { |
| files.add(new File(filePath)); |
| } |
| } |
| return files.toArray(new File[files.size()]); |
| } |
| |
| /** Invokes {@link File#renameTo(File)} on the given files. */ |
| @Override |
| public boolean renameTo(@NonNull File oldFile, @NonNull File newFile) { |
| boolean renamed = false; |
| |
| String oldPath = getAgnosticAbsPath(oldFile); |
| String newPath = getAgnosticAbsPath(newFile); |
| Pattern pathRE = Pattern.compile( |
| "^(" + Pattern.quote(oldPath) + //$NON-NLS-1$ |
| ")($|/.*)"); //$NON-NLS-1$ |
| |
| Set<String> newFolders = Sets.newTreeSet(); |
| for (Iterator<String> it = mExistingFolders.iterator(); it.hasNext(); ) { |
| String folder = it.next(); |
| Matcher m = pathRE.matcher(folder); |
| if (m.matches()) { |
| it.remove(); |
| String newFolder = newPath + m.group(2); |
| newFolders.add(newFolder); |
| renamed = true; |
| } |
| } |
| mExistingFolders.addAll(newFolders); |
| newFolders.clear(); |
| |
| Map<String, FileInfo> newFiles = Maps.newTreeMap(); |
| for (Iterator<Entry<String, FileInfo>> it = mExistingFiles.entrySet().iterator(); |
| it.hasNext(); ) { |
| Entry<String, FileInfo> entry = it.next(); |
| String filePath = entry.getKey(); |
| Matcher m = pathRE.matcher(filePath); |
| if (m.matches()) { |
| it.remove(); |
| String newFilePath = newPath + m.group(2); |
| newFiles.put(newFilePath, entry.getValue()); |
| renamed = true; |
| } |
| } |
| mExistingFiles.putAll(newFiles); |
| |
| return renamed; |
| } |
| |
| /** |
| * {@inheritDoc} |
| * <p/> |
| * <em>TODO: we might want to overload this to read mock properties instead of a real file.</em> |
| */ |
| @NonNull |
| @Override |
| public Properties loadProperties(@NonNull File file) { |
| Properties props = new Properties(); |
| FileInputStream fis = null; |
| try { |
| fis = new FileInputStream(file); |
| props.load(fis); |
| } catch (IOException ignore) { |
| } finally { |
| if (fis != null) { |
| try { |
| fis.close(); |
| } catch (Exception ignore) {} |
| } |
| } |
| return props; |
| } |
| |
| /** |
| * {@inheritDoc} |
| * <p/> |
| * <em>Note that this uses the mock version of {@link #newFileOutputStream(File)} and thus |
| * records the write rather than actually performing it.</em> |
| */ |
| @Override |
| public void saveProperties( |
| @NonNull File file, |
| @NonNull Properties props, |
| @NonNull String comments) throws IOException { |
| OutputStream fos = null; |
| try { |
| fos = newFileOutputStream(file); |
| props.store(fos, comments); |
| } finally { |
| if (fos != null) { |
| try { |
| fos.close(); |
| } catch (IOException e) { |
| } |
| } |
| } |
| } |
| |
| /** |
| * Returns an OutputStream that will capture the bytes written and associate |
| * them with the given file. |
| */ |
| @NonNull |
| @Override |
| public OutputStream newFileOutputStream(@NonNull File file) throws FileNotFoundException { |
| StringOutputStream os = new StringOutputStream(file); |
| mOutputStreams.add(os); |
| return os; |
| } |
| |
| /** |
| * An {@link OutputStream} that will capture the stream as an UTF-8 string once properly closed |
| * and associate it to the given {@link File}. |
| */ |
| public class StringOutputStream extends ByteArrayOutputStream { |
| private String mData; |
| private final File mFile; |
| |
| public StringOutputStream(File file) { |
| mFile = file; |
| recordExistingFile(file); |
| } |
| |
| public File getFile() { |
| return mFile; |
| } |
| |
| /** Can be null if the stream has never been properly closed. */ |
| public String getData() { |
| return mData; |
| } |
| |
| /** Once the stream is properly closed, convert the byte array to an UTF-8 string */ |
| @Override |
| public void close() throws IOException { |
| super.close(); |
| mData = new String(toByteArray(), "UTF-8"); //$NON-NLS-1$ |
| } |
| |
| /** Returns a string representation suitable for unit tests validation. */ |
| @Override |
| public synchronized String toString() { |
| StringBuilder sb = new StringBuilder(); |
| sb.append('<').append(getAgnosticAbsPath(mFile)).append(": "); //$NON-NLS-1$ |
| if (mData == null) { |
| sb.append("(stream not closed properly)>"); //$NON-NLS-1$ |
| } else { |
| sb.append('\'').append(mData).append("'>"); //$NON-NLS-1$ |
| } |
| return sb.toString(); |
| } |
| } |
| |
| @NonNull |
| @Override |
| public InputStream newFileInputStream(@NonNull File file) throws FileNotFoundException { |
| FileInfo fi = mExistingFiles.get(getAgnosticAbsPath(file)); |
| if (fi != null) { |
| byte[] content = fi.getContent(); |
| if (content != null) { |
| return new ByteArrayInputStream(content); |
| } |
| } |
| throw new FileNotFoundException("Mock file has no content: " + getAgnosticAbsPath(file)); |
| } |
| |
| @Override |
| public long lastModified(@NonNull File file) { |
| FileInfo fi = mExistingFiles.get(getAgnosticAbsPath(file)); |
| if (fi != null) { |
| return fi.getLastModified(); |
| } |
| return 0; |
| } |
| |
| // ----- |
| |
| private static class FileInfo { |
| private long mLastModified; |
| private byte[] mContent; |
| |
| public FileInfo(long lastModified, @Nullable byte[] content) { |
| mLastModified = lastModified; |
| mContent = content; |
| } |
| |
| public long getLastModified() { |
| return mLastModified; |
| } |
| |
| @Nullable |
| public byte[] getContent() { |
| return mContent; |
| } |
| |
| } |
| } |