Home | History | Annotate | Download | only in jnigen
      1 /*******************************************************************************
      2  * Copyright 2011 See AUTHORS file.
      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.badlogic.gdx.jnigen;
     18 
     19 import java.io.BufferedReader;
     20 import java.io.File;
     21 import java.io.FileInputStream;
     22 import java.io.FileNotFoundException;
     23 import java.io.FileOutputStream;
     24 import java.io.IOException;
     25 import java.io.InputStream;
     26 import java.io.InputStreamReader;
     27 import java.io.OutputStream;
     28 import java.io.OutputStreamWriter;
     29 import java.io.Reader;
     30 import java.io.UnsupportedEncodingException;
     31 import java.io.Writer;
     32 
     33 /** Represents a file or directory on the filesystem or classpath. Taken from libgdx's FileHandle.
     34  * @author mzechner
     35  * @author Nathan Sweet */
     36 public class FileDescriptor {
     37 	/** Indicates how to resolve a path to a file.
     38 	 * @author mzechner
     39 	 * @author Nathan Sweet */
     40 	public enum FileType {
     41 		/** Path relative to the root of the classpath. Classpath files are always readonly. Note that classpath files are not
     42 		 * compatible with some functionality on Android, such as Audio#newSound(FileHandle) and Audio#newMusic(FileHandle). */
     43 		Classpath,
     44 
     45 		/** Path that is a fully qualified, absolute filesystem path. To ensure portability across platforms use absolute files only
     46 		 * when absolutely (heh) necessary. */
     47 		Absolute;
     48 	}
     49 
     50 	protected File file;
     51 	protected FileType type;
     52 
     53 	protected FileDescriptor () {
     54 	}
     55 
     56 	/** Creates a new absolute FileHandle for the file name. Use this for tools on the desktop that don't need any of the backends.
     57 	 * Do not use this constructor in case you write something cross-platform. Use the Files interface instead.
     58 	 * @param fileName the filename. */
     59 	public FileDescriptor (String fileName) {
     60 		this.file = new File(fileName);
     61 		this.type = FileType.Absolute;
     62 	}
     63 
     64 	/** Creates a new absolute FileHandle for the {@link File}. Use this for tools on the desktop that don't need any of the
     65 	 * backends. Do not use this constructor in case you write something cross-platform. Use the Files interface instead.
     66 	 * @param file the file. */
     67 	public FileDescriptor (File file) {
     68 		this.file = file;
     69 		this.type = FileType.Absolute;
     70 	}
     71 
     72 	protected FileDescriptor (String fileName, FileType type) {
     73 		this.type = type;
     74 		file = new File(fileName);
     75 	}
     76 
     77 	protected FileDescriptor (File file, FileType type) {
     78 		this.file = file;
     79 		this.type = type;
     80 	}
     81 
     82 	public String path () {
     83 		return file.getPath().replace('\\', '/');
     84 	}
     85 
     86 	public String name () {
     87 		return file.getName();
     88 	}
     89 
     90 	public String extension () {
     91 		String name = file.getName();
     92 		int dotIndex = name.lastIndexOf('.');
     93 		if (dotIndex == -1) return "";
     94 		return name.substring(dotIndex + 1);
     95 	}
     96 
     97 	public String nameWithoutExtension () {
     98 		String name = file.getName();
     99 		int dotIndex = name.lastIndexOf('.');
    100 		if (dotIndex == -1) return name;
    101 		return name.substring(0, dotIndex);
    102 	}
    103 
    104 	public FileType type () {
    105 		return type;
    106 	}
    107 
    108 	/** Returns a java.io.File that represents this file handle. Note the returned file will only be usable for
    109 	 * {@link FileType#Absolute} and FileType#External file handles. */
    110 	public File file () {
    111 		return file;
    112 	}
    113 
    114 	/** Returns a stream for reading this file as bytes.
    115 	 * @throw RuntimeException if the file handle represents a directory, doesn't exist, or could not be read. */
    116 	public InputStream read () {
    117 		if (type == FileType.Classpath && !file.exists()) {
    118 			InputStream input = FileDescriptor.class.getResourceAsStream("/" + file.getPath().replace('\\', '/'));
    119 			if (input == null) throw new RuntimeException("File not found: " + file + " (" + type + ")");
    120 			return input;
    121 		}
    122 		try {
    123 			return new FileInputStream(file());
    124 		} catch (FileNotFoundException ex) {
    125 			if (file().isDirectory())
    126 				throw new RuntimeException("Cannot open a stream to a directory: " + file + " (" + type + ")", ex);
    127 			throw new RuntimeException("Error reading file: " + file + " (" + type + ")", ex);
    128 		}
    129 	}
    130 
    131 	/** Returns a reader for reading this file as characters.
    132 	 * @throw RuntimeException if the file handle represents a directory, doesn't exist, or could not be read. */
    133 	public Reader reader () {
    134 		return new InputStreamReader(read());
    135 	}
    136 
    137 	/** Returns a reader for reading this file as characters.
    138 	 * @throw RuntimeException if the file handle represents a directory, doesn't exist, or could not be read. */
    139 	public Reader reader (String charset) {
    140 		try {
    141 			return new InputStreamReader(read(), charset);
    142 		} catch (UnsupportedEncodingException ex) {
    143 			throw new RuntimeException("Error reading file: " + this, ex);
    144 		}
    145 	}
    146 
    147 	/** Returns a buffered reader for reading this file as characters.
    148 	 * @throw RuntimeException if the file handle represents a directory, doesn't exist, or could not be read. */
    149 	public BufferedReader reader (int bufferSize) {
    150 		return new BufferedReader(new InputStreamReader(read()), bufferSize);
    151 	}
    152 
    153 	/** Returns a buffered reader for reading this file as characters.
    154 	 * @throw RuntimeException if the file handle represents a directory, doesn't exist, or could not be read. */
    155 	public BufferedReader reader (int bufferSize, String charset) {
    156 		try {
    157 			return new BufferedReader(new InputStreamReader(read(), charset), bufferSize);
    158 		} catch (UnsupportedEncodingException ex) {
    159 			throw new RuntimeException("Error reading file: " + this, ex);
    160 		}
    161 	}
    162 
    163 	/** Reads the entire file into a string using the platform's default charset.
    164 	 * @throw RuntimeException if the file handle represents a directory, doesn't exist, or could not be read. */
    165 	public String readString () {
    166 		return readString(null);
    167 	}
    168 
    169 	/** Reads the entire file into a string using the specified charset.
    170 	 * @throw RuntimeException if the file handle represents a directory, doesn't exist, or could not be read. */
    171 	public String readString (String charset) {
    172 		StringBuilder output = new StringBuilder(512);
    173 		InputStreamReader reader = null;
    174 		try {
    175 			if (charset == null)
    176 				reader = new InputStreamReader(read());
    177 			else
    178 				reader = new InputStreamReader(read(), charset);
    179 			char[] buffer = new char[256];
    180 			while (true) {
    181 				int length = reader.read(buffer);
    182 				if (length == -1) break;
    183 				output.append(buffer, 0, length);
    184 			}
    185 		} catch (IOException ex) {
    186 			throw new RuntimeException("Error reading layout file: " + this, ex);
    187 		} finally {
    188 			try {
    189 				if (reader != null) reader.close();
    190 			} catch (IOException ignored) {
    191 			}
    192 		}
    193 		return output.toString();
    194 	}
    195 
    196 	/** Reads the entire file into a byte array.
    197 	 * @throw RuntimeException if the file handle represents a directory, doesn't exist, or could not be read. */
    198 	public byte[] readBytes () {
    199 		int length = (int)length();
    200 		if (length == 0) length = 512;
    201 		byte[] buffer = new byte[length];
    202 		int position = 0;
    203 		InputStream input = read();
    204 		try {
    205 			while (true) {
    206 				int count = input.read(buffer, position, buffer.length - position);
    207 				if (count == -1) break;
    208 				position += count;
    209 				if (position == buffer.length) {
    210 					// Grow buffer.
    211 					byte[] newBuffer = new byte[buffer.length * 2];
    212 					System.arraycopy(buffer, 0, newBuffer, 0, position);
    213 					buffer = newBuffer;
    214 				}
    215 			}
    216 		} catch (IOException ex) {
    217 			throw new RuntimeException("Error reading file: " + this, ex);
    218 		} finally {
    219 			try {
    220 				if (input != null) input.close();
    221 			} catch (IOException ignored) {
    222 			}
    223 		}
    224 		if (position < buffer.length) {
    225 			// Shrink buffer.
    226 			byte[] newBuffer = new byte[position];
    227 			System.arraycopy(buffer, 0, newBuffer, 0, position);
    228 			buffer = newBuffer;
    229 		}
    230 		return buffer;
    231 	}
    232 
    233 	/** Reads the entire file into the byte array. The byte array must be big enough to hold the file's data.
    234 	 * @param bytes the array to load the file into
    235 	 * @param offset the offset to start writing bytes
    236 	 * @param size the number of bytes to read, see {@link #length()}
    237 	 * @return the number of read bytes */
    238 	public int readBytes (byte[] bytes, int offset, int size) {
    239 		InputStream input = read();
    240 		int position = 0;
    241 		try {
    242 			while (true) {
    243 				int count = input.read(bytes, offset + position, size - position);
    244 				if (count <= 0) break;
    245 				position += count;
    246 			}
    247 		} catch (IOException ex) {
    248 			throw new RuntimeException("Error reading file: " + this, ex);
    249 		} finally {
    250 			try {
    251 				if (input != null) input.close();
    252 			} catch (IOException ignored) {
    253 			}
    254 		}
    255 		return position - offset;
    256 	}
    257 
    258 	/** Returns a stream for writing to this file. Parent directories will be created if necessary.
    259 	 * @param append If false, this file will be overwritten if it exists, otherwise it will be appended.
    260 	 * @throw RuntimeException if this file handle represents a directory, if it is a {@link FileType#Classpath} or
    261 	 *        FileType#Internal file, or if it could not be written. */
    262 	public OutputStream write (boolean append) {
    263 		if (type == FileType.Classpath) throw new RuntimeException("Cannot write to a classpath file: " + file);
    264 		parent().mkdirs();
    265 		try {
    266 			return new FileOutputStream(file(), append);
    267 		} catch (FileNotFoundException ex) {
    268 			if (file().isDirectory())
    269 				throw new RuntimeException("Cannot open a stream to a directory: " + file + " (" + type + ")", ex);
    270 			throw new RuntimeException("Error writing file: " + file + " (" + type + ")", ex);
    271 		}
    272 	}
    273 
    274 	/** Reads the remaining bytes from the specified stream and writes them to this file. The stream is closed. Parent directories
    275 	 * will be created if necessary.
    276 	 * @param append If false, this file will be overwritten if it exists, otherwise it will be appended.
    277 	 * @throw RuntimeException if this file handle represents a directory, if it is a {@link FileType#Classpath} or
    278 	 *        FileType#Internal file, or if it could not be written. */
    279 	public void write (InputStream input, boolean append) {
    280 		OutputStream output = null;
    281 		try {
    282 			output = write(append);
    283 			byte[] buffer = new byte[4096];
    284 			while (true) {
    285 				int length = input.read(buffer);
    286 				if (length == -1) break;
    287 				output.write(buffer, 0, length);
    288 			}
    289 		} catch (Exception ex) {
    290 			throw new RuntimeException("Error stream writing to file: " + file + " (" + type + ")", ex);
    291 		} finally {
    292 			try {
    293 				if (input != null) input.close();
    294 			} catch (Exception ignored) {
    295 			}
    296 			try {
    297 				if (output != null) output.close();
    298 			} catch (Exception ignored) {
    299 			}
    300 		}
    301 
    302 	}
    303 
    304 	/** Returns a writer for writing to this file using the default charset. Parent directories will be created if necessary.
    305 	 * @param append If false, this file will be overwritten if it exists, otherwise it will be appended.
    306 	 * @throw RuntimeException if this file handle represents a directory, if it is a {@link FileType#Classpath} or
    307 	 *        FileType#Internal file, or if it could not be written. */
    308 	public Writer writer (boolean append) {
    309 		return writer(append, null);
    310 	}
    311 
    312 	/** Returns a writer for writing to this file. Parent directories will be created if necessary.
    313 	 * @param append If false, this file will be overwritten if it exists, otherwise it will be appended.
    314 	 * @param charset May be null to use the default charset.
    315 	 * @throw RuntimeException if this file handle represents a directory, if it is a {@link FileType#Classpath} or
    316 	 *        FileType#Internal file, or if it could not be written. */
    317 	public Writer writer (boolean append, String charset) {
    318 		if (type == FileType.Classpath) throw new RuntimeException("Cannot write to a classpath file: " + file);
    319 		parent().mkdirs();
    320 		try {
    321 			FileOutputStream output = new FileOutputStream(file(), append);
    322 			if (charset == null)
    323 				return new OutputStreamWriter(output);
    324 			else
    325 				return new OutputStreamWriter(output, charset);
    326 		} catch (IOException ex) {
    327 			if (file().isDirectory())
    328 				throw new RuntimeException("Cannot open a stream to a directory: " + file + " (" + type + ")", ex);
    329 			throw new RuntimeException("Error writing file: " + file + " (" + type + ")", ex);
    330 		}
    331 	}
    332 
    333 	/** Writes the specified string to the file using the default charset. Parent directories will be created if necessary.
    334 	 * @param append If false, this file will be overwritten if it exists, otherwise it will be appended.
    335 	 * @throw RuntimeException if this file handle represents a directory, if it is a {@link FileType#Classpath} or
    336 	 *        FileType#Internal file, or if it could not be written. */
    337 	public void writeString (String string, boolean append) {
    338 		writeString(string, append, null);
    339 	}
    340 
    341 	/** Writes the specified string to the file as UTF-8. Parent directories will be created if necessary.
    342 	 * @param append If false, this file will be overwritten if it exists, otherwise it will be appended.
    343 	 * @param charset May be null to use the default charset.
    344 	 * @throw RuntimeException if this file handle represents a directory, if it is a {@link FileType#Classpath} or
    345 	 *        FileType#Internal file, or if it could not be written. */
    346 	public void writeString (String string, boolean append, String charset) {
    347 		Writer writer = null;
    348 		try {
    349 			writer = writer(append, charset);
    350 			writer.write(string);
    351 		} catch (Exception ex) {
    352 			throw new RuntimeException("Error writing file: " + file + " (" + type + ")", ex);
    353 		} finally {
    354 			try {
    355 				if (writer != null) writer.close();
    356 			} catch (Exception ignored) {
    357 			}
    358 		}
    359 	}
    360 
    361 	/** Writes the specified bytes to the file. Parent directories will be created if necessary.
    362 	 * @param append If false, this file will be overwritten if it exists, otherwise it will be appended.
    363 	 * @throw RuntimeException if this file handle represents a directory, if it is a {@link FileType#Classpath} or
    364 	 *        FileType#Internal file, or if it could not be written. */
    365 	public void writeBytes (byte[] bytes, boolean append) {
    366 		OutputStream output = write(append);
    367 		try {
    368 			output.write(bytes);
    369 		} catch (IOException ex) {
    370 			throw new RuntimeException("Error writing file: " + file + " (" + type + ")", ex);
    371 		} finally {
    372 			try {
    373 				output.close();
    374 			} catch (IOException ignored) {
    375 			}
    376 		}
    377 	}
    378 
    379 	/** Returns the paths to the children of this directory. Returns an empty list if this file handle represents a file and not a
    380 	 * directory. On the desktop, an FileType#Internal handle to a directory on the classpath will return a zero length array.
    381 	 * @throw RuntimeException if this file is an {@link FileType#Classpath} file. */
    382 	public FileDescriptor[] list () {
    383 		if (type == FileType.Classpath) throw new RuntimeException("Cannot list a classpath directory: " + file);
    384 		String[] relativePaths = file().list();
    385 		if (relativePaths == null) return new FileDescriptor[0];
    386 		FileDescriptor[] handles = new FileDescriptor[relativePaths.length];
    387 		for (int i = 0, n = relativePaths.length; i < n; i++)
    388 			handles[i] = child(relativePaths[i]);
    389 		return handles;
    390 	}
    391 
    392 	/** Returns the paths to the children of this directory with the specified suffix. Returns an empty list if this file handle
    393 	 * represents a file and not a directory. On the desktop, an FileType#Internal handle to a directory on the classpath will
    394 	 * return a zero length array.
    395 	 * @throw RuntimeException if this file is an {@link FileType#Classpath} file. */
    396 	public FileDescriptor[] list (String suffix) {
    397 		if (type == FileType.Classpath) throw new RuntimeException("Cannot list a classpath directory: " + file);
    398 		String[] relativePaths = file().list();
    399 		if (relativePaths == null) return new FileDescriptor[0];
    400 		FileDescriptor[] handles = new FileDescriptor[relativePaths.length];
    401 		int count = 0;
    402 		for (int i = 0, n = relativePaths.length; i < n; i++) {
    403 			String path = relativePaths[i];
    404 			if (!path.endsWith(suffix)) continue;
    405 			handles[count] = child(path);
    406 			count++;
    407 		}
    408 		if (count < relativePaths.length) {
    409 			FileDescriptor[] newHandles = new FileDescriptor[count];
    410 			System.arraycopy(handles, 0, newHandles, 0, count);
    411 			handles = newHandles;
    412 		}
    413 		return handles;
    414 	}
    415 
    416 	/** Returns true if this file is a directory. Always returns false for classpath files. On Android, an FileType#Internal handle
    417 	 * to an empty directory will return false. On the desktop, an FileType#Internal handle to a directory on the classpath will
    418 	 * return false. */
    419 	public boolean isDirectory () {
    420 		if (type == FileType.Classpath) return false;
    421 		return file().isDirectory();
    422 	}
    423 
    424 	/** Returns a handle to the child with the specified name.
    425 	 * @throw RuntimeException if this file handle is a {@link FileType#Classpath} or FileType#Internal and the child doesn't
    426 	 *        exist. */
    427 	public FileDescriptor child (String name) {
    428 		if (file.getPath().length() == 0) return new FileDescriptor(new File(name), type);
    429 		return new FileDescriptor(new File(file, name), type);
    430 	}
    431 
    432 	public FileDescriptor parent () {
    433 		File parent = file.getParentFile();
    434 		if (parent == null) {
    435 			if (type == FileType.Absolute)
    436 				parent = new File("/");
    437 			else
    438 				parent = new File("");
    439 		}
    440 		return new FileDescriptor(parent, type);
    441 	}
    442 
    443 	/** @throw RuntimeException if this file handle is a {@link FileType#Classpath} or FileType#Internal file. */
    444 	public boolean mkdirs () {
    445 		if (type == FileType.Classpath) throw new RuntimeException("Cannot mkdirs with a classpath file: " + file);
    446 		return file().mkdirs();
    447 	}
    448 
    449 	/** Returns true if the file exists. On Android, a {@link FileType#Classpath} or FileType#Internal handle to a directory will
    450 	 * always return false. */
    451 	public boolean exists () {
    452 		if (type == FileType.Classpath) return FileDescriptor.class.getResource("/" + file.getPath().replace('\\', '/')) != null;
    453 		return file().exists();
    454 	}
    455 
    456 	/** Deletes this file or empty directory and returns success. Will not delete a directory that has children.
    457 	 * @throw RuntimeException if this file handle is a {@link FileType#Classpath} or FileType#Internal file. */
    458 	public boolean delete () {
    459 		if (type == FileType.Classpath) throw new RuntimeException("Cannot delete a classpath file: " + file);
    460 		return file().delete();
    461 	}
    462 
    463 	/** Deletes this file or directory and all children, recursively.
    464 	 * @throw RuntimeException if this file handle is a {@link FileType#Classpath} or FileType#Internal file. */
    465 	public boolean deleteDirectory () {
    466 		if (type == FileType.Classpath) throw new RuntimeException("Cannot delete a classpath file: " + file);
    467 		return deleteDirectory(file());
    468 	}
    469 
    470 	/** Copies this file or directory to the specified file or directory. If this handle is a file, then 1) if the destination is a
    471 	 * file, it is overwritten, or 2) if the destination is a directory, this file is copied into it, or 3) if the destination
    472 	 * doesn't exist, {@link #mkdirs()} is called on the destination's parent and this file is copied into it with a new name. If
    473 	 * this handle is a directory, then 1) if the destination is a file, RuntimeException is thrown, or 2) if the destination is a
    474 	 * directory, this directory is copied recursively into it as a subdirectory, overwriting existing files, or 3) if the
    475 	 * destination doesn't exist, {@link #mkdirs()} is called on the destination and this directory is copied recursively into it
    476 	 * as a subdirectory.
    477 	 * @throw RuntimeException if the destination file handle is a {@link FileType#Classpath} or FileType#Internal file, or copying
    478 	 *        failed. */
    479 	public void copyTo (FileDescriptor dest) {
    480 		if (!isDirectory()) {
    481 			if (dest.isDirectory()) dest = dest.child(name());
    482 			copyFile(this, dest);
    483 			return;
    484 		}
    485 		if (dest.exists()) {
    486 			if (!dest.isDirectory()) throw new RuntimeException("Destination exists but is not a directory: " + dest);
    487 		} else {
    488 			dest.mkdirs();
    489 			if (!dest.isDirectory()) throw new RuntimeException("Destination directory cannot be created: " + dest);
    490 		}
    491 		dest = dest.child(name());
    492 		copyDirectory(this, dest);
    493 	}
    494 
    495 	/** Moves this file to the specified file, overwriting the file if it already exists.
    496 	 * @throw RuntimeException if the source or destination file handle is a {@link FileType#Classpath} or FileType#Internal file. */
    497 	public void moveTo (FileDescriptor dest) {
    498 		if (type == FileType.Classpath) throw new RuntimeException("Cannot move a classpath file: " + file);
    499 		copyTo(dest);
    500 		delete();
    501 	}
    502 
    503 	/** Returns the length in bytes of this file, or 0 if this file is a directory, does not exist, or the size cannot otherwise be
    504 	 * determined. */
    505 	public long length () {
    506 		if (type == FileType.Classpath || !file.exists()) {
    507 			InputStream input = read();
    508 			try {
    509 				return input.available();
    510 			} catch (Exception ignored) {
    511 			} finally {
    512 				try {
    513 					input.close();
    514 				} catch (IOException ignored) {
    515 				}
    516 			}
    517 			return 0;
    518 		}
    519 		return file().length();
    520 	}
    521 
    522 	/** Returns the last modified time in milliseconds for this file. Zero is returned if the file doesn't exist. Zero is returned
    523 	 * for {@link FileType#Classpath} files. On Android, zero is returned for FileType#Internal files. On the desktop, zero is
    524 	 * returned for FileType#Internal files on the classpath. */
    525 	public long lastModified () {
    526 		return file().lastModified();
    527 	}
    528 
    529 	public String toString () {
    530 		return file.getPath();
    531 	}
    532 
    533 	static public FileDescriptor tempFile (String prefix) {
    534 		try {
    535 			return new FileDescriptor(File.createTempFile(prefix, null));
    536 		} catch (IOException ex) {
    537 			throw new RuntimeException("Unable to create temp file.", ex);
    538 		}
    539 	}
    540 
    541 	static public FileDescriptor tempDirectory (String prefix) {
    542 		try {
    543 			File file = File.createTempFile(prefix, null);
    544 			if (!file.delete()) throw new IOException("Unable to delete temp file: " + file);
    545 			if (!file.mkdir()) throw new IOException("Unable to create temp directory: " + file);
    546 			return new FileDescriptor(file);
    547 		} catch (IOException ex) {
    548 			throw new RuntimeException("Unable to create temp file.", ex);
    549 		}
    550 	}
    551 
    552 	static private boolean deleteDirectory (File file) {
    553 		if (file.exists()) {
    554 			File[] files = file.listFiles();
    555 			if (files != null) {
    556 				for (int i = 0, n = files.length; i < n; i++) {
    557 					if (files[i].isDirectory())
    558 						deleteDirectory(files[i]);
    559 					else
    560 						files[i].delete();
    561 				}
    562 			}
    563 		}
    564 		return file.delete();
    565 	}
    566 
    567 	static private void copyFile (FileDescriptor source, FileDescriptor dest) {
    568 		try {
    569 			dest.write(source.read(), false);
    570 		} catch (Exception ex) {
    571 			throw new RuntimeException("Error copying source file: " + source.file + " (" + source.type + ")\n" //
    572 				+ "To destination: " + dest.file + " (" + dest.type + ")", ex);
    573 		}
    574 	}
    575 
    576 	static private void copyDirectory (FileDescriptor sourceDir, FileDescriptor destDir) {
    577 		destDir.mkdirs();
    578 		FileDescriptor[] files = sourceDir.list();
    579 		for (int i = 0, n = files.length; i < n; i++) {
    580 			FileDescriptor srcFile = files[i];
    581 			FileDescriptor destFile = destDir.child(srcFile.name());
    582 			if (srcFile.isDirectory())
    583 				copyDirectory(srcFile, destFile);
    584 			else
    585 				copyFile(srcFile, destFile);
    586 		}
    587 	}
    588 }
    589