/******************************************************************************* * Copyright 2011 See AUTHORS file. * * 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.badlogic.gdx.files; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.BufferedReader; import java.io.File; import java.io.FileFilter; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.FilenameFilter; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.Reader; import java.io.UnsupportedEncodingException; import java.io.Writer; import com.badlogic.gdx.Files; import com.badlogic.gdx.Files.FileType; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.utils.GdxRuntimeException; import com.badlogic.gdx.utils.StreamUtils; /** Represents a file or directory on the filesystem, classpath, Android SD card, or Android assets directory. FileHandles are * created via a {@link Files} instance. * * Because some of the file types are backed by composite files and may be compressed (for example, if they are in an Android .apk * or are found via the classpath), the methods for extracting a {@link #path()} or {@link #file()} may not be appropriate for all * types. Use the Reader or Stream methods here to hide these dependencies from your platform independent code. * * @author mzechner * @author Nathan Sweet */ public class FileHandle { protected File file; protected FileType type; protected FileHandle () { } /** Creates a new absolute FileHandle for the file name. Use this for tools on the desktop that don't need any of the backends. * Do not use this constructor in case you write something cross-platform. Use the {@link Files} interface instead. * @param fileName the filename. */ public FileHandle (String fileName) { this.file = new File(fileName); this.type = FileType.Absolute; } /** Creates a new absolute FileHandle for the {@link File}. Use this for tools on the desktop that don't need any of the * backends. Do not use this constructor in case you write something cross-platform. Use the {@link Files} interface instead. * @param file the file. */ public FileHandle (File file) { this.file = file; this.type = FileType.Absolute; } protected FileHandle (String fileName, FileType type) { this.type = type; file = new File(fileName); } protected FileHandle (File file, FileType type) { this.file = file; this.type = type; } /** @return the path of the file as specified on construction, e.g. Gdx.files.internal("dir/file.png") -> dir/file.png. * backward slashes will be replaced by forward slashes. */ public String path () { return file.getPath().replace('\\', '/'); } /** @return the name of the file, without any parent paths. */ public String name () { return file.getName(); } public String extension () { String name = file.getName(); int dotIndex = name.lastIndexOf('.'); if (dotIndex == -1) return ""; return name.substring(dotIndex + 1); } /** @return the name of the file, without parent paths or the extension. */ public String nameWithoutExtension () { String name = file.getName(); int dotIndex = name.lastIndexOf('.'); if (dotIndex == -1) return name; return name.substring(0, dotIndex); } /** @return the path and filename without the extension, e.g. dir/dir2/file.png -> dir/dir2/file. backward slashes will be * returned as forward slashes. */ public String pathWithoutExtension () { String path = file.getPath().replace('\\', '/'); int dotIndex = path.lastIndexOf('.'); if (dotIndex == -1) return path; return path.substring(0, dotIndex); } public FileType type () { return type; } /** Returns a java.io.File that represents this file handle. Note the returned file will only be usable for * {@link FileType#Absolute} and {@link FileType#External} file handles. */ public File file () { if (type == FileType.External) return new File(Gdx.files.getExternalStoragePath(), file.getPath()); return file; } /** Returns a stream for reading this file as bytes. * @throws GdxRuntimeException if the file handle represents a directory, doesn't exist, or could not be read. */ public InputStream read () { if (type == FileType.Classpath || (type == FileType.Internal && !file().exists()) || (type == FileType.Local && !file().exists())) { InputStream input = FileHandle.class.getResourceAsStream("/" + file.getPath().replace('\\', '/')); if (input == null) throw new GdxRuntimeException("File not found: " + file + " (" + type + ")"); return input; } try { return new FileInputStream(file()); } catch (Exception ex) { if (file().isDirectory()) throw new GdxRuntimeException("Cannot open a stream to a directory: " + file + " (" + type + ")", ex); throw new GdxRuntimeException("Error reading file: " + file + " (" + type + ")", ex); } } /** Returns a buffered stream for reading this file as bytes. * @throws GdxRuntimeException if the file handle represents a directory, doesn't exist, or could not be read. */ public BufferedInputStream read (int bufferSize) { return new BufferedInputStream(read(), bufferSize); } /** Returns a reader for reading this file as characters. * @throws GdxRuntimeException if the file handle represents a directory, doesn't exist, or could not be read. */ public Reader reader () { return new InputStreamReader(read()); } /** Returns a reader for reading this file as characters. * @throws GdxRuntimeException if the file handle represents a directory, doesn't exist, or could not be read. */ public Reader reader (String charset) { InputStream stream = read(); try { return new InputStreamReader(stream, charset); } catch (UnsupportedEncodingException ex) { StreamUtils.closeQuietly(stream); throw new GdxRuntimeException("Error reading file: " + this, ex); } } /** Returns a buffered reader for reading this file as characters. * @throws GdxRuntimeException if the file handle represents a directory, doesn't exist, or could not be read. */ public BufferedReader reader (int bufferSize) { return new BufferedReader(new InputStreamReader(read()), bufferSize); } /** Returns a buffered reader for reading this file as characters. * @throws GdxRuntimeException if the file handle represents a directory, doesn't exist, or could not be read. */ public BufferedReader reader (int bufferSize, String charset) { try { return new BufferedReader(new InputStreamReader(read(), charset), bufferSize); } catch (UnsupportedEncodingException ex) { throw new GdxRuntimeException("Error reading file: " + this, ex); } } /** Reads the entire file into a string using the platform's default charset. * @throws GdxRuntimeException if the file handle represents a directory, doesn't exist, or could not be read. */ public String readString () { return readString(null); } /** Reads the entire file into a string using the specified charset. * @param charset If null the default charset is used. * @throws GdxRuntimeException if the file handle represents a directory, doesn't exist, or could not be read. */ public String readString (String charset) { StringBuilder output = new StringBuilder(estimateLength()); InputStreamReader reader = null; try { if (charset == null) reader = new InputStreamReader(read()); else reader = new InputStreamReader(read(), charset); char[] buffer = new char[256]; while (true) { int length = reader.read(buffer); if (length == -1) break; output.append(buffer, 0, length); } } catch (IOException ex) { throw new GdxRuntimeException("Error reading layout file: " + this, ex); } finally { StreamUtils.closeQuietly(reader); } return output.toString(); } /** Reads the entire file into a byte array. * @throws GdxRuntimeException if the file handle represents a directory, doesn't exist, or could not be read. */ public byte[] readBytes () { InputStream input = read(); try { return StreamUtils.copyStreamToByteArray(input, estimateLength()); } catch (IOException ex) { throw new GdxRuntimeException("Error reading file: " + this, ex); } finally { StreamUtils.closeQuietly(input); } } private int estimateLength () { int length = (int)length(); return length != 0 ? length : 512; } /** Reads the entire file into the byte array. The byte array must be big enough to hold the file's data. * @param bytes the array to load the file into * @param offset the offset to start writing bytes * @param size the number of bytes to read, see {@link #length()} * @return the number of read bytes */ public int readBytes (byte[] bytes, int offset, int size) { InputStream input = read(); int position = 0; try { while (true) { int count = input.read(bytes, offset + position, size - position); if (count <= 0) break; position += count; } } catch (IOException ex) { throw new GdxRuntimeException("Error reading file: " + this, ex); } finally { StreamUtils.closeQuietly(input); } return position - offset; } /** Returns a stream for writing to this file. Parent directories will be created if necessary. * @param append If false, this file will be overwritten if it exists, otherwise it will be appended. * @throws GdxRuntimeException if this file handle represents a directory, if it is a {@link FileType#Classpath} or * {@link FileType#Internal} file, or if it could not be written. */ public OutputStream write (boolean append) { if (type == FileType.Classpath) throw new GdxRuntimeException("Cannot write to a classpath file: " + file); if (type == FileType.Internal) throw new GdxRuntimeException("Cannot write to an internal file: " + file); parent().mkdirs(); try { return new FileOutputStream(file(), append); } catch (Exception ex) { if (file().isDirectory()) throw new GdxRuntimeException("Cannot open a stream to a directory: " + file + " (" + type + ")", ex); throw new GdxRuntimeException("Error writing file: " + file + " (" + type + ")", ex); } } /** Returns a buffered stream for writing to this file. Parent directories will be created if necessary. * @param append If false, this file will be overwritten if it exists, otherwise it will be appended. * @param bufferSize The size of the buffer. * @throws GdxRuntimeException if this file handle represents a directory, if it is a {@link FileType#Classpath} or * {@link FileType#Internal} file, or if it could not be written. */ public OutputStream write (boolean append, int bufferSize) { return new BufferedOutputStream(write(append), bufferSize); } /** Reads the remaining bytes from the specified stream and writes them to this file. The stream is closed. Parent directories * will be created if necessary. * @param append If false, this file will be overwritten if it exists, otherwise it will be appended. * @throws GdxRuntimeException if this file handle represents a directory, if it is a {@link FileType#Classpath} or * {@link FileType#Internal} file, or if it could not be written. */ public void write (InputStream input, boolean append) { OutputStream output = null; try { output = write(append); StreamUtils.copyStream(input, output); } catch (Exception ex) { throw new GdxRuntimeException("Error stream writing to file: " + file + " (" + type + ")", ex); } finally { StreamUtils.closeQuietly(input); StreamUtils.closeQuietly(output); } } /** Returns a writer for writing to this file using the default charset. Parent directories will be created if necessary. * @param append If false, this file will be overwritten if it exists, otherwise it will be appended. * @throws GdxRuntimeException if this file handle represents a directory, if it is a {@link FileType#Classpath} or * {@link FileType#Internal} file, or if it could not be written. */ public Writer writer (boolean append) { return writer(append, null); } /** Returns a writer for writing to this file. Parent directories will be created if necessary. * @param append If false, this file will be overwritten if it exists, otherwise it will be appended. * @param charset May be null to use the default charset. * @throws GdxRuntimeException if this file handle represents a directory, if it is a {@link FileType#Classpath} or * {@link FileType#Internal} file, or if it could not be written. */ public Writer writer (boolean append, String charset) { if (type == FileType.Classpath) throw new GdxRuntimeException("Cannot write to a classpath file: " + file); if (type == FileType.Internal) throw new GdxRuntimeException("Cannot write to an internal file: " + file); parent().mkdirs(); try { FileOutputStream output = new FileOutputStream(file(), append); if (charset == null) return new OutputStreamWriter(output); else return new OutputStreamWriter(output, charset); } catch (IOException ex) { if (file().isDirectory()) throw new GdxRuntimeException("Cannot open a stream to a directory: " + file + " (" + type + ")", ex); throw new GdxRuntimeException("Error writing file: " + file + " (" + type + ")", ex); } } /** Writes the specified string to the file using the default charset. Parent directories will be created if necessary. * @param append If false, this file will be overwritten if it exists, otherwise it will be appended. * @throws GdxRuntimeException if this file handle represents a directory, if it is a {@link FileType#Classpath} or * {@link FileType#Internal} file, or if it could not be written. */ public void writeString (String string, boolean append) { writeString(string, append, null); } /** Writes the specified string to the file using the specified charset. Parent directories will be created if necessary. * @param append If false, this file will be overwritten if it exists, otherwise it will be appended. * @param charset May be null to use the default charset. * @throws GdxRuntimeException if this file handle represents a directory, if it is a {@link FileType#Classpath} or * {@link FileType#Internal} file, or if it could not be written. */ public void writeString (String string, boolean append, String charset) { Writer writer = null; try { writer = writer(append, charset); writer.write(string); } catch (Exception ex) { throw new GdxRuntimeException("Error writing file: " + file + " (" + type + ")", ex); } finally { StreamUtils.closeQuietly(writer); } } /** Writes the specified bytes to the file. Parent directories will be created if necessary. * @param append If false, this file will be overwritten if it exists, otherwise it will be appended. * @throws GdxRuntimeException if this file handle represents a directory, if it is a {@link FileType#Classpath} or * {@link FileType#Internal} file, or if it could not be written. */ public void writeBytes (byte[] bytes, boolean append) { OutputStream output = write(append); try { output.write(bytes); } catch (IOException ex) { throw new GdxRuntimeException("Error writing file: " + file + " (" + type + ")", ex); } finally { StreamUtils.closeQuietly(output); } } /** Writes the specified bytes to the file. Parent directories will be created if necessary. * @param append If false, this file will be overwritten if it exists, otherwise it will be appended. * @throws GdxRuntimeException if this file handle represents a directory, if it is a {@link FileType#Classpath} or * {@link FileType#Internal} file, or if it could not be written. */ public void writeBytes (byte[] bytes, int offset, int length, boolean append) { OutputStream output = write(append); try { output.write(bytes, offset, length); } catch (IOException ex) { throw new GdxRuntimeException("Error writing file: " + file + " (" + type + ")", ex); } finally { StreamUtils.closeQuietly(output); } } /** Returns the paths to the children of this directory. Returns an empty list if this file handle represents a file and not a * directory. On the desktop, an {@link FileType#Internal} handle to a directory on the classpath will return a zero length * array. * @throws GdxRuntimeException if this file is an {@link FileType#Classpath} file. */ public FileHandle[] list () { if (type == FileType.Classpath) throw new GdxRuntimeException("Cannot list a classpath directory: " + file); String[] relativePaths = file().list(); if (relativePaths == null) return new FileHandle[0]; FileHandle[] handles = new FileHandle[relativePaths.length]; for (int i = 0, n = relativePaths.length; i < n; i++) handles[i] = child(relativePaths[i]); return handles; } /** Returns the paths to the children of this directory that satisfy the specified filter. Returns an empty list if this file * handle represents a file and not a directory. On the desktop, an {@link FileType#Internal} handle to a directory on the * classpath will return a zero length array. * @param filter the {@link FileFilter} to filter files * @throws GdxRuntimeException if this file is an {@link FileType#Classpath} file. */ public FileHandle[] list (FileFilter filter) { if (type == FileType.Classpath) throw new GdxRuntimeException("Cannot list a classpath directory: " + file); File file = file(); String[] relativePaths = file.list(); if (relativePaths == null) return new FileHandle[0]; FileHandle[] handles = new FileHandle[relativePaths.length]; int count = 0; for (int i = 0, n = relativePaths.length; i < n; i++) { String path = relativePaths[i]; FileHandle child = child(path); if (!filter.accept(child.file())) continue; handles[count] = child; count++; } if (count < relativePaths.length) { FileHandle[] newHandles = new FileHandle[count]; System.arraycopy(handles, 0, newHandles, 0, count); handles = newHandles; } return handles; } /** Returns the paths to the children of this directory that satisfy the specified filter. Returns an empty list if this file * handle represents a file and not a directory. On the desktop, an {@link FileType#Internal} handle to a directory on the * classpath will return a zero length array. * @param filter the {@link FilenameFilter} to filter files * @throws GdxRuntimeException if this file is an {@link FileType#Classpath} file. */ public FileHandle[] list (FilenameFilter filter) { if (type == FileType.Classpath) throw new GdxRuntimeException("Cannot list a classpath directory: " + file); File file = file(); String[] relativePaths = file.list(); if (relativePaths == null) return new FileHandle[0]; FileHandle[] handles = new FileHandle[relativePaths.length]; int count = 0; for (int i = 0, n = relativePaths.length; i < n; i++) { String path = relativePaths[i]; if (!filter.accept(file, path)) continue; handles[count] = child(path); count++; } if (count < relativePaths.length) { FileHandle[] newHandles = new FileHandle[count]; System.arraycopy(handles, 0, newHandles, 0, count); handles = newHandles; } return handles; } /** Returns the paths to the children of this directory with the specified suffix. Returns an empty list if this file handle * represents a file and not a directory. On the desktop, an {@link FileType#Internal} handle to a directory on the classpath * will return a zero length array. * @throws GdxRuntimeException if this file is an {@link FileType#Classpath} file. */ public FileHandle[] list (String suffix) { if (type == FileType.Classpath) throw new GdxRuntimeException("Cannot list a classpath directory: " + file); String[] relativePaths = file().list(); if (relativePaths == null) return new FileHandle[0]; FileHandle[] handles = new FileHandle[relativePaths.length]; int count = 0; for (int i = 0, n = relativePaths.length; i < n; i++) { String path = relativePaths[i]; if (!path.endsWith(suffix)) continue; handles[count] = child(path); count++; } if (count < relativePaths.length) { FileHandle[] newHandles = new FileHandle[count]; System.arraycopy(handles, 0, newHandles, 0, count); handles = newHandles; } return handles; } /** Returns true if this file is a directory. Always returns false for classpath files. On Android, an * {@link FileType#Internal} handle to an empty directory will return false. On the desktop, an {@link FileType#Internal} * handle to a directory on the classpath will return false. */ public boolean isDirectory () { if (type == FileType.Classpath) return false; return file().isDirectory(); } /** Returns a handle to the child with the specified name. */ public FileHandle child (String name) { if (file.getPath().length() == 0) return new FileHandle(new File(name), type); return new FileHandle(new File(file, name), type); } /** Returns a handle to the sibling with the specified name. * @throws GdxRuntimeException if this file is the root. */ public FileHandle sibling (String name) { if (file.getPath().length() == 0) throw new GdxRuntimeException("Cannot get the sibling of the root."); return new FileHandle(new File(file.getParent(), name), type); } public FileHandle parent () { File parent = file.getParentFile(); if (parent == null) { if (type == FileType.Absolute) parent = new File("/"); else parent = new File(""); } return new FileHandle(parent, type); } /** @throws GdxRuntimeException if this file handle is a {@link FileType#Classpath} or {@link FileType#Internal} file. */ public void mkdirs () { if (type == FileType.Classpath) throw new GdxRuntimeException("Cannot mkdirs with a classpath file: " + file); if (type == FileType.Internal) throw new GdxRuntimeException("Cannot mkdirs with an internal file: " + file); file().mkdirs(); } /** Returns true if the file exists. On Android, a {@link FileType#Classpath} or {@link FileType#Internal} handle to a * directory will always return false. Note that this can be very slow for internal files on Android! */ public boolean exists () { switch (type) { case Internal: if (file().exists()) return true; // Fall through. case Classpath: return FileHandle.class.getResource("/" + file.getPath().replace('\\', '/')) != null; } return file().exists(); } /** Deletes this file or empty directory and returns success. Will not delete a directory that has children. * @throws GdxRuntimeException if this file handle is a {@link FileType#Classpath} or {@link FileType#Internal} file. */ public boolean delete () { if (type == FileType.Classpath) throw new GdxRuntimeException("Cannot delete a classpath file: " + file); if (type == FileType.Internal) throw new GdxRuntimeException("Cannot delete an internal file: " + file); return file().delete(); } /** Deletes this file or directory and all children, recursively. * @throws GdxRuntimeException if this file handle is a {@link FileType#Classpath} or {@link FileType#Internal} file. */ public boolean deleteDirectory () { if (type == FileType.Classpath) throw new GdxRuntimeException("Cannot delete a classpath file: " + file); if (type == FileType.Internal) throw new GdxRuntimeException("Cannot delete an internal file: " + file); return deleteDirectory(file()); } /** Deletes all children of this directory, recursively. * @throws GdxRuntimeException if this file handle is a {@link FileType#Classpath} or {@link FileType#Internal} file. */ public void emptyDirectory () { emptyDirectory(false); } /** Deletes all children of this directory, recursively. Optionally preserving the folder structure. * @throws GdxRuntimeException if this file handle is a {@link FileType#Classpath} or {@link FileType#Internal} file. */ public void emptyDirectory (boolean preserveTree) { if (type == FileType.Classpath) throw new GdxRuntimeException("Cannot delete a classpath file: " + file); if (type == FileType.Internal) throw new GdxRuntimeException("Cannot delete an internal file: " + file); emptyDirectory(file(), preserveTree); } /** Copies this file or directory to the specified file or directory. If this handle is a file, then 1) if the destination is a * file, it is overwritten, or 2) if the destination is a directory, this file is copied into it, or 3) if the destination * doesn't exist, {@link #mkdirs()} is called on the destination's parent and this file is copied into it with a new name. If * this handle is a directory, then 1) if the destination is a file, GdxRuntimeException is thrown, or 2) if the destination is * a directory, this directory is copied into it recursively, overwriting existing files, or 3) if the destination doesn't * exist, {@link #mkdirs()} is called on the destination and this directory is copied into it recursively. * @throws GdxRuntimeException if the destination file handle is a {@link FileType#Classpath} or {@link FileType#Internal} * file, or copying failed. */ public void copyTo (FileHandle dest) { if (!isDirectory()) { if (dest.isDirectory()) dest = dest.child(name()); copyFile(this, dest); return; } if (dest.exists()) { if (!dest.isDirectory()) throw new GdxRuntimeException("Destination exists but is not a directory: " + dest); } else { dest.mkdirs(); if (!dest.isDirectory()) throw new GdxRuntimeException("Destination directory cannot be created: " + dest); } copyDirectory(this, dest.child(name())); } /** Moves this file to the specified file, overwriting the file if it already exists. * @throws GdxRuntimeException if the source or destination file handle is a {@link FileType#Classpath} or * {@link FileType#Internal} file. */ public void moveTo (FileHandle dest) { if (type == FileType.Classpath) throw new GdxRuntimeException("Cannot move a classpath file: " + file); if (type == FileType.Internal) throw new GdxRuntimeException("Cannot move an internal file: " + file); copyTo(dest); delete(); if (exists() && isDirectory()) deleteDirectory(); } /** 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 * determined. */ public long length () { if (type == FileType.Classpath || (type == FileType.Internal && !file.exists())) { InputStream input = read(); try { return input.available(); } catch (Exception ignored) { } finally { StreamUtils.closeQuietly(input); } return 0; } return file().length(); } /** Returns the last modified time in milliseconds for this file. Zero is returned if the file doesn't exist. Zero is returned * for {@link FileType#Classpath} files. On Android, zero is returned for {@link FileType#Internal} files. On the desktop, zero * is returned for {@link FileType#Internal} files on the classpath. */ public long lastModified () { return file().lastModified(); } @Override public boolean equals (Object obj) { if (!(obj instanceof FileHandle)) return false; FileHandle other = (FileHandle)obj; return type == other.type && path().equals(other.path()); } @Override public int hashCode () { int hash = 1; hash = hash * 37 + type.hashCode(); hash = hash * 67 + path().hashCode(); return hash; } public String toString () { return file.getPath().replace('\\', '/'); } static public FileHandle tempFile (String prefix) { try { return new FileHandle(File.createTempFile(prefix, null)); } catch (IOException ex) { throw new GdxRuntimeException("Unable to create temp file.", ex); } } static public FileHandle tempDirectory (String prefix) { try { File file = File.createTempFile(prefix, null); if (!file.delete()) throw new IOException("Unable to delete temp file: " + file); if (!file.mkdir()) throw new IOException("Unable to create temp directory: " + file); return new FileHandle(file); } catch (IOException ex) { throw new GdxRuntimeException("Unable to create temp file.", ex); } } static private void emptyDirectory (File file, boolean preserveTree) { if (file.exists()) { File[] files = file.listFiles(); if (files != null) { for (int i = 0, n = files.length; i < n; i++) { if (!files[i].isDirectory()) files[i].delete(); else if (preserveTree) emptyDirectory(files[i], true); else deleteDirectory(files[i]); } } } } static private boolean deleteDirectory (File file) { emptyDirectory(file, false); return file.delete(); } static private void copyFile (FileHandle source, FileHandle dest) { try { dest.write(source.read(), false); } catch (Exception ex) { throw new GdxRuntimeException("Error copying source file: " + source.file + " (" + source.type + ")\n" // + "To destination: " + dest.file + " (" + dest.type + ")", ex); } } static private void copyDirectory (FileHandle sourceDir, FileHandle destDir) { destDir.mkdirs(); FileHandle[] files = sourceDir.list(); for (int i = 0, n = files.length; i < n; i++) { FileHandle srcFile = files[i]; FileHandle destFile = destDir.child(srcFile.name()); if (srcFile.isDirectory()) copyDirectory(srcFile, destFile); else copyFile(srcFile, destFile); } } }