• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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