1 /* 2 * Copyright (C) 2011 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.sdklib.io; 18 19 import com.android.SdkConstants; 20 import com.android.annotations.NonNull; 21 22 import java.io.ByteArrayOutputStream; 23 import java.io.File; 24 import java.io.FileInputStream; 25 import java.io.FileNotFoundException; 26 import java.io.IOException; 27 import java.io.OutputStream; 28 import java.util.ArrayList; 29 import java.util.HashSet; 30 import java.util.Iterator; 31 import java.util.List; 32 import java.util.Properties; 33 import java.util.Set; 34 import java.util.TreeSet; 35 import java.util.regex.Matcher; 36 import java.util.regex.Pattern; 37 38 39 /** 40 * Mock version of {@link FileOp} that wraps some common {@link File} 41 * operations on files and folders. 42 * <p/> 43 * This version does not perform any file operation. Instead it records a textual 44 * representation of all the file operations performed. 45 * <p/> 46 * To avoid cross-platform path issues (e.g. Windows path), the methods here should 47 * always use rooted (aka absolute) unix-looking paths, e.g. "/dir1/dir2/file3". 48 * When processing {@link File}, you can convert them using {@link #getAgnosticAbsPath(File)}. 49 */ 50 public class MockFileOp implements IFileOp { 51 52 private final Set<String> mExistinfFiles = new TreeSet<String>(); 53 private final Set<String> mExistinfFolders = new TreeSet<String>(); 54 private final List<StringOutputStream> mOutputStreams = new ArrayList<StringOutputStream>(); 55 MockFileOp()56 public MockFileOp() { 57 } 58 59 /** Resets the internal state, as if the object had been newly created. */ reset()60 public void reset() { 61 mExistinfFiles.clear(); 62 mExistinfFolders.clear(); 63 } 64 getAgnosticAbsPath(File file)65 public String getAgnosticAbsPath(File file) { 66 return getAgnosticAbsPath(file.getAbsolutePath()); 67 } 68 getAgnosticAbsPath(String path)69 public String getAgnosticAbsPath(String path) { 70 if (SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_WINDOWS) { 71 // Try to convert the windows-looking path to a unix-looking one 72 path = path.replace('\\', '/'); 73 path = path.replace("C:", ""); //$NON-NLS-1$ //$NON-NLS-2$ 74 } 75 return path; 76 } 77 78 /** 79 * Records a new absolute file path. 80 * Parent folders are not automatically created. 81 */ recordExistingFile(File file)82 public void recordExistingFile(File file) { 83 mExistinfFiles.add(getAgnosticAbsPath(file)); 84 } 85 86 /** 87 * Records a new absolute file path. 88 * Parent folders are not automatically created. 89 * <p/> 90 * The syntax should always look "unix-like", e.g. "/dir/file". 91 * On Windows that means you'll want to use {@link #getAgnosticAbsPath(File)}. 92 * @param absFilePath A unix-like file path, e.g. "/dir/file" 93 */ recordExistingFile(String absFilePath)94 public void recordExistingFile(String absFilePath) { 95 mExistinfFiles.add(absFilePath); 96 } 97 98 /** 99 * Records a new absolute folder path. 100 * Parent folders are not automatically created. 101 */ recordExistingFolder(File folder)102 public void recordExistingFolder(File folder) { 103 mExistinfFolders.add(getAgnosticAbsPath(folder)); 104 } 105 106 /** 107 * Records a new absolute folder path. 108 * Parent folders are not automatically created. 109 * <p/> 110 * The syntax should always look "unix-like", e.g. "/dir/file". 111 * On Windows that means you'll want to use {@link #getAgnosticAbsPath(File)}. 112 * @param absFolderPath A unix-like folder path, e.g. "/dir/file" 113 */ recordExistingFolder(String absFolderPath)114 public void recordExistingFolder(String absFolderPath) { 115 mExistinfFolders.add(absFolderPath); 116 } 117 118 /** 119 * Returns the list of paths added using {@link #recordExistingFile(String)} 120 * and eventually updated by {@link #delete(File)} operations. 121 * <p/> 122 * The returned list is sorted by alphabetic absolute path string. 123 */ getExistingFiles()124 public String[] getExistingFiles() { 125 return mExistinfFiles.toArray(new String[mExistinfFiles.size()]); 126 } 127 128 /** 129 * Returns the list of folder paths added using {@link #recordExistingFolder(String)} 130 * and eventually updated {@link #delete(File)} or {@link #mkdirs(File)} operations. 131 * <p/> 132 * The returned list is sorted by alphabetic absolute path string. 133 */ getExistingFolders()134 public String[] getExistingFolders() { 135 return mExistinfFolders.toArray(new String[mExistinfFolders.size()]); 136 } 137 138 /** 139 * Returns the {@link StringOutputStream#toString()} as an array, in creation order. 140 * Array can be empty but not null. 141 */ getOutputStreams()142 public String[] getOutputStreams() { 143 int n = mOutputStreams.size(); 144 String[] result = new String[n]; 145 for (int i = 0; i < n; i++) { 146 result[i] = mOutputStreams.get(i).toString(); 147 } 148 return result; 149 } 150 151 /** 152 * Helper to delete a file or a directory. 153 * For a directory, recursively deletes all of its content. 154 * Files that cannot be deleted right away are marked for deletion on exit. 155 * The argument can be null. 156 */ 157 @Override deleteFileOrFolder(File fileOrFolder)158 public void deleteFileOrFolder(File fileOrFolder) { 159 if (fileOrFolder != null) { 160 if (isDirectory(fileOrFolder)) { 161 // Must delete content recursively first 162 for (File item : listFiles(fileOrFolder)) { 163 deleteFileOrFolder(item); 164 } 165 } 166 delete(fileOrFolder); 167 } 168 } 169 170 /** 171 * {@inheritDoc} 172 * <p/> 173 * <em>Note: this mock version does nothing.</em> 174 */ 175 @Override setExecutablePermission(File file)176 public void setExecutablePermission(File file) throws IOException { 177 // pass 178 } 179 180 /** 181 * {@inheritDoc} 182 * <p/> 183 * <em>Note: this mock version does nothing.</em> 184 */ 185 @Override setReadOnly(File file)186 public void setReadOnly(File file) { 187 // pass 188 } 189 190 /** 191 * {@inheritDoc} 192 * <p/> 193 * <em>Note: this mock version does nothing.</em> 194 */ 195 @Override copyFile(File source, File dest)196 public void copyFile(File source, File dest) throws IOException { 197 // pass 198 } 199 200 /** 201 * Checks whether 2 binary files are the same. 202 * 203 * @param source the source file to copy 204 * @param destination the destination file to write 205 * @throws FileNotFoundException if the source files don't exist. 206 * @throws IOException if there's a problem reading the files. 207 */ 208 @Override isSameFile(File source, File destination)209 public boolean isSameFile(File source, File destination) throws IOException { 210 throw new UnsupportedOperationException("MockFileUtils.isSameFile is not supported."); //$NON-NLS-1$ 211 } 212 213 /** Invokes {@link File#isFile()} on the given {@code file}. */ 214 @Override isFile(File file)215 public boolean isFile(File file) { 216 String path = getAgnosticAbsPath(file); 217 return mExistinfFiles.contains(path); 218 } 219 220 /** Invokes {@link File#isDirectory()} on the given {@code file}. */ 221 @Override isDirectory(File file)222 public boolean isDirectory(File file) { 223 String path = getAgnosticAbsPath(file); 224 if (mExistinfFolders.contains(path)) { 225 return true; 226 } 227 228 // If we defined a file or folder as a child of the requested file path, 229 // then the directory exists implicitely. 230 Pattern pathRE = Pattern.compile( 231 Pattern.quote(path + (path.endsWith("/") ? "" : '/')) + //$NON-NLS-1$ //$NON-NLS-2$ 232 ".*"); //$NON-NLS-1$ 233 234 for (String folder : mExistinfFolders) { 235 if (pathRE.matcher(folder).matches()) { 236 return true; 237 } 238 } 239 for (String filePath : mExistinfFiles) { 240 if (pathRE.matcher(filePath).matches()) { 241 return true; 242 } 243 } 244 245 return false; 246 } 247 248 /** Invokes {@link File#exists()} on the given {@code file}. */ 249 @Override exists(File file)250 public boolean exists(File file) { 251 return isFile(file) || isDirectory(file); 252 } 253 254 /** Invokes {@link File#length()} on the given {@code file}. */ 255 @Override length(File file)256 public long length(File file) { 257 throw new UnsupportedOperationException("MockFileUtils.length is not supported."); //$NON-NLS-1$ 258 } 259 260 @Override delete(File file)261 public boolean delete(File file) { 262 String path = getAgnosticAbsPath(file); 263 264 if (mExistinfFiles.remove(path)) { 265 return true; 266 } 267 268 boolean hasSubfiles = false; 269 for (String folder : mExistinfFolders) { 270 if (folder.startsWith(path) && !folder.equals(path)) { 271 // the File.delete operation is not recursive and would fail to remove 272 // a root dir that is not empty. 273 return false; 274 } 275 } 276 if (!hasSubfiles) { 277 for (String filePath : mExistinfFiles) { 278 if (filePath.startsWith(path) && !filePath.equals(path)) { 279 // the File.delete operation is not recursive and would fail to remove 280 // a root dir that is not empty. 281 return false; 282 } 283 } 284 } 285 286 return mExistinfFolders.remove(path); 287 } 288 289 /** Invokes {@link File#mkdirs()} on the given {@code file}. */ 290 @Override mkdirs(File file)291 public boolean mkdirs(File file) { 292 for (; file != null; file = file.getParentFile()) { 293 String path = getAgnosticAbsPath(file); 294 mExistinfFolders.add(path); 295 } 296 return true; 297 } 298 299 /** 300 * Invokes {@link File#listFiles()} on the given {@code file}. 301 * The returned list is sorted by alphabetic absolute path string. 302 */ 303 @Override listFiles(File file)304 public File[] listFiles(File file) { 305 TreeSet<File> files = new TreeSet<File>(); 306 307 String path = getAgnosticAbsPath(file); 308 Pattern pathRE = Pattern.compile( 309 Pattern.quote(path + (path.endsWith("/") ? "" : '/')) + //$NON-NLS-1$ //$NON-NLS-2$ 310 ".*"); //$NON-NLS-1$ 311 312 for (String folder : mExistinfFolders) { 313 if (pathRE.matcher(folder).matches()) { 314 files.add(new File(folder)); 315 } 316 } 317 for (String filePath : mExistinfFiles) { 318 if (pathRE.matcher(filePath).matches()) { 319 files.add(new File(filePath)); 320 } 321 } 322 return files.toArray(new File[files.size()]); 323 } 324 325 /** Invokes {@link File#renameTo(File)} on the given files. */ 326 @Override renameTo(File oldFile, File newFile)327 public boolean renameTo(File oldFile, File newFile) { 328 boolean renamed = false; 329 330 String oldPath = getAgnosticAbsPath(oldFile); 331 String newPath = getAgnosticAbsPath(newFile); 332 Pattern pathRE = Pattern.compile( 333 "^(" + Pattern.quote(oldPath) + //$NON-NLS-1$ 334 ")($|/.*)"); //$NON-NLS-1$ 335 336 Set<String> newStrings = new HashSet<String>(); 337 for (Iterator<String> it = mExistinfFolders.iterator(); it.hasNext(); ) { 338 String folder = it.next(); 339 Matcher m = pathRE.matcher(folder); 340 if (m.matches()) { 341 it.remove(); 342 String newFolder = newPath + m.group(2); 343 newStrings.add(newFolder); 344 renamed = true; 345 } 346 } 347 mExistinfFolders.addAll(newStrings); 348 newStrings.clear(); 349 350 for (Iterator<String> it = mExistinfFiles.iterator(); it.hasNext(); ) { 351 String filePath = it.next(); 352 Matcher m = pathRE.matcher(filePath); 353 if (m.matches()) { 354 it.remove(); 355 String newFilePath = newPath + m.group(2); 356 newStrings.add(newFilePath); 357 renamed = true; 358 } 359 } 360 mExistinfFiles.addAll(newStrings); 361 362 return renamed; 363 } 364 365 /** 366 * {@inheritDoc} 367 * <p/> 368 * <em>TODO: we might want to overload this to read mock properties instead of a real file.</em> 369 */ 370 @Override loadProperties(@onNull File file)371 public @NonNull Properties loadProperties(@NonNull File file) { 372 Properties props = new Properties(); 373 FileInputStream fis = null; 374 try { 375 fis = new FileInputStream(file); 376 props.load(fis); 377 } catch (IOException ignore) { 378 } finally { 379 if (fis != null) { 380 try { 381 fis.close(); 382 } catch (Exception ignore) {} 383 } 384 } 385 return props; 386 } 387 388 /** 389 * {@inheritDoc} 390 * <p/> 391 * <em>Note that this uses the mock version of {@link #newFileOutputStream(File)} and thus 392 * records the write rather than actually performing it.</em> 393 */ 394 @Override saveProperties(@onNull File file, @NonNull Properties props, @NonNull String comments)395 public boolean saveProperties(@NonNull File file, @NonNull Properties props, 396 @NonNull String comments) { 397 OutputStream fos = null; 398 try { 399 fos = newFileOutputStream(file); 400 401 props.store(fos, comments); 402 return true; 403 } catch (IOException ignore) { 404 } finally { 405 if (fos != null) { 406 try { 407 fos.close(); 408 } catch (IOException e) { 409 } 410 } 411 } 412 413 return false; 414 } 415 416 /** 417 * Returns an OutputStream that will capture the bytes written and associate 418 * them with the given file. 419 */ 420 @Override newFileOutputStream(File file)421 public OutputStream newFileOutputStream(File file) throws FileNotFoundException { 422 StringOutputStream os = new StringOutputStream(file); 423 mOutputStreams.add(os); 424 return os; 425 } 426 427 /** 428 * An {@link OutputStream} that will capture the stream as an UTF-8 string once properly closed 429 * and associate it to the given {@link File}. 430 */ 431 public class StringOutputStream extends ByteArrayOutputStream { 432 private String mData; 433 private final File mFile; 434 StringOutputStream(File file)435 public StringOutputStream(File file) { 436 mFile = file; 437 recordExistingFile(file); 438 } 439 getFile()440 public File getFile() { 441 return mFile; 442 } 443 444 /** Can be null if the stream has never been properly closed. */ getData()445 public String getData() { 446 return mData; 447 } 448 449 /** Once the stream is properly closed, convert the byte array to an UTF-8 string */ 450 @Override close()451 public void close() throws IOException { 452 super.close(); 453 mData = new String(toByteArray(), "UTF-8"); //$NON-NLS-1$ 454 } 455 456 /** Returns a string representation suitable for unit tests validation. */ 457 @Override toString()458 public synchronized String toString() { 459 StringBuilder sb = new StringBuilder(); 460 sb.append('<').append(getAgnosticAbsPath(mFile)).append(": "); //$NON-NLS-1$ 461 if (mData == null) { 462 sb.append("(stream not closed properly)>"); //$NON-NLS-1$ 463 } else { 464 sb.append('\'').append(mData).append("'>"); //$NON-NLS-1$ 465 } 466 return sb.toString(); 467 } 468 } 469 } 470