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