/* * 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 java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.OutputStream; import java.util.ArrayList; import java.util.HashSet; import java.util.Iterator; import java.util.List; 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 Set<String> mExistinfFiles = new TreeSet<String>(); private final Set<String> mExistinfFolders = new TreeSet<String>(); 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() { mExistinfFiles.clear(); mExistinfFolders.clear(); } public String getAgnosticAbsPath(File file) { return getAgnosticAbsPath(file.getAbsolutePath()); } public String getAgnosticAbsPath(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.replace("C:", ""); //$NON-NLS-1$ //$NON-NLS-2$ } return path; } /** * Records a new absolute file path. * Parent folders are not automatically created. */ public void recordExistingFile(File file) { mExistinfFiles.add(getAgnosticAbsPath(file)); } /** * 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(String absFilePath) { mExistinfFiles.add(absFilePath); } /** * Records a new absolute folder path. * Parent folders are not automatically created. */ public void recordExistingFolder(File folder) { mExistinfFolders.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) { mExistinfFolders.add(absFolderPath); } /** * 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. */ public String[] getExistingFiles() { return mExistinfFiles.toArray(new String[mExistinfFiles.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. */ public String[] getExistingFolders() { return mExistinfFolders.toArray(new String[mExistinfFolders.size()]); } /** * Returns the {@link StringOutputStream#toString()} as an array, in creation order. * Array can be empty but not null. */ 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(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(File file) throws IOException { // pass } /** * {@inheritDoc} * <p/> * <em>Note: this mock version does nothing.</em> */ @Override public void setReadOnly(File file) { // pass } /** * {@inheritDoc} * <p/> * <em>Note: this mock version does nothing.</em> */ @Override public void copyFile(File source, File dest) throws IOException { // pass } /** * Checks whether 2 binary files are the same. * * @param source the source file to copy * @param destination 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(File source, File destination) throws IOException { throw new UnsupportedOperationException("MockFileUtils.isSameFile is not supported."); //$NON-NLS-1$ } /** Invokes {@link File#isFile()} on the given {@code file}. */ @Override public boolean isFile(File file) { String path = getAgnosticAbsPath(file); return mExistinfFiles.contains(path); } /** Invokes {@link File#isDirectory()} on the given {@code file}. */ @Override public boolean isDirectory(File file) { String path = getAgnosticAbsPath(file); if (mExistinfFolders.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 : mExistinfFolders) { if (pathRE.matcher(folder).matches()) { return true; } } for (String filePath : mExistinfFiles) { if (pathRE.matcher(filePath).matches()) { return true; } } return false; } /** Invokes {@link File#exists()} on the given {@code file}. */ @Override public boolean exists(File file) { return isFile(file) || isDirectory(file); } /** Invokes {@link File#length()} on the given {@code file}. */ @Override public long length(File file) { throw new UnsupportedOperationException("MockFileUtils.length is not supported."); //$NON-NLS-1$ } @Override public boolean delete(File file) { String path = getAgnosticAbsPath(file); if (mExistinfFiles.remove(path)) { return true; } boolean hasSubfiles = false; for (String folder : mExistinfFolders) { 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 : mExistinfFiles) { 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 mExistinfFolders.remove(path); } /** Invokes {@link File#mkdirs()} on the given {@code file}. */ @Override public boolean mkdirs(File file) { for (; file != null; file = file.getParentFile()) { String path = getAgnosticAbsPath(file); mExistinfFolders.add(path); } return true; } /** * Invokes {@link File#listFiles()} on the given {@code file}. * The returned list is sorted by alphabetic absolute path string. */ @Override public File[] listFiles(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 : mExistinfFolders) { if (pathRE.matcher(folder).matches()) { files.add(new File(folder)); } } for (String filePath : mExistinfFiles) { 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(File oldFile, 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> newStrings = new HashSet<String>(); for (Iterator<String> it = mExistinfFolders.iterator(); it.hasNext(); ) { String folder = it.next(); Matcher m = pathRE.matcher(folder); if (m.matches()) { it.remove(); String newFolder = newPath + m.group(2); newStrings.add(newFolder); renamed = true; } } mExistinfFolders.addAll(newStrings); newStrings.clear(); for (Iterator<String> it = mExistinfFiles.iterator(); it.hasNext(); ) { String filePath = it.next(); Matcher m = pathRE.matcher(filePath); if (m.matches()) { it.remove(); String newFilePath = newPath + m.group(2); newStrings.add(newFilePath); renamed = true; } } mExistinfFiles.addAll(newStrings); return renamed; } /** * {@inheritDoc} * <p/> * <em>TODO: we might want to overload this to read mock properties instead of a real file.</em> */ @Override public @NonNull 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 boolean saveProperties(@NonNull File file, @NonNull Properties props, @NonNull String comments) { OutputStream fos = null; try { fos = newFileOutputStream(file); props.store(fos, comments); return true; } catch (IOException ignore) { } finally { if (fos != null) { try { fos.close(); } catch (IOException e) { } } } return false; } /** * Returns an OutputStream that will capture the bytes written and associate * them with the given file. */ @Override public OutputStream newFileOutputStream(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(); } } }