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
17package com.badlogic.gdx.backends.gwt;
18
19import java.io.BufferedInputStream;
20import java.io.BufferedReader;
21import java.io.File;
22import java.io.FileFilter;
23import java.io.FilenameFilter;
24import java.io.IOException;
25import java.io.InputStream;
26import java.io.InputStreamReader;
27import java.io.OutputStream;
28import java.io.Reader;
29import java.io.UnsupportedEncodingException;
30import java.io.Writer;
31
32import com.badlogic.gdx.Files.FileType;
33import com.badlogic.gdx.Gdx;
34import com.badlogic.gdx.backends.gwt.preloader.Preloader;
35import com.badlogic.gdx.files.FileHandle;
36import com.badlogic.gdx.utils.GdxRuntimeException;
37
38public class GwtFileHandle extends FileHandle {
39	public final Preloader preloader;
40	private final String file;
41	private final FileType type;
42
43	public GwtFileHandle (Preloader preloader, String fileName, FileType type) {
44		if (type != FileType.Internal && type != FileType.Classpath)
45			throw new GdxRuntimeException("FileType '" + type + "' Not supported in GWT backend");
46		this.preloader = preloader;
47		this.file = fixSlashes(fileName);
48		this.type = type;
49	}
50
51	public GwtFileHandle (String path) {
52		this.type = FileType.Internal;
53		this.preloader = ((GwtApplication)Gdx.app).getPreloader();
54		this.file = fixSlashes(path);
55	}
56
57	public String path () {
58		return file;
59	}
60
61	public String name () {
62		int index = file.lastIndexOf('/');
63		if (index < 0) return file;
64		return file.substring(index + 1);
65	}
66
67	public String extension () {
68		String name = name();
69		int dotIndex = name.lastIndexOf('.');
70		if (dotIndex == -1) return "";
71		return name.substring(dotIndex + 1);
72	}
73
74	public String nameWithoutExtension () {
75		String name = name();
76		int dotIndex = name.lastIndexOf('.');
77		if (dotIndex == -1) return name;
78		return name.substring(0, dotIndex);
79	}
80
81	/** @return the path and filename without the extension, e.g. dir/dir2/file.png -> dir/dir2/file */
82	public String pathWithoutExtension () {
83		String path = file;
84		int dotIndex = path.lastIndexOf('.');
85		if (dotIndex == -1) return path;
86		return path.substring(0, dotIndex);
87	}
88
89	public FileType type () {
90		return type;
91	}
92
93	/** Returns a java.io.File that represents this file handle. Note the returned file will only be usable for
94	 * {@link FileType#Absolute} and {@link FileType#External} file handles. */
95	public File file () {
96		throw new GdxRuntimeException("Not supported in GWT backend");
97	}
98
99	/** Returns a stream for reading this file as bytes.
100	 * @throw GdxRuntimeException if the file handle represents a directory, doesn't exist, or could not be read. */
101	public InputStream read () {
102		InputStream in = preloader.read(file);
103		if (in == null) throw new GdxRuntimeException(file + " does not exist");
104		return in;
105	}
106
107	/** Returns a buffered stream for reading this file as bytes.
108	 * @throw GdxRuntimeException if the file handle represents a directory, doesn't exist, or could not be read. */
109	public BufferedInputStream read (int bufferSize) {
110		return new BufferedInputStream(read(), bufferSize);
111	}
112
113	/** Returns a reader for reading this file as characters.
114	 * @throw GdxRuntimeException if the file handle represents a directory, doesn't exist, or could not be read. */
115	public Reader reader () {
116		return new InputStreamReader(read());
117	}
118
119	/** Returns a reader for reading this file as characters.
120	 * @throw GdxRuntimeException if the file handle represents a directory, doesn't exist, or could not be read. */
121	public Reader reader (String charset) {
122		try {
123			return new InputStreamReader(read(), charset);
124		} catch (UnsupportedEncodingException e) {
125			throw new GdxRuntimeException("Encoding '" + charset + "' not supported", e);
126		}
127	}
128
129	/** Returns a buffered reader for reading this file as characters.
130	 * @throw GdxRuntimeException if the file handle represents a directory, doesn't exist, or could not be read. */
131	public BufferedReader reader (int bufferSize) {
132		return new BufferedReader(reader(), bufferSize);
133	}
134
135	/** Returns a buffered reader for reading this file as characters.
136	 * @throw GdxRuntimeException if the file handle represents a directory, doesn't exist, or could not be read. */
137	public BufferedReader reader (int bufferSize, String charset) {
138		return new BufferedReader(reader(charset), bufferSize);
139	}
140
141	/** Reads the entire file into a string using the platform's default charset.
142	 * @throw GdxRuntimeException if the file handle represents a directory, doesn't exist, or could not be read. */
143	public String readString () {
144		return readString(null);
145	}
146
147	/** Reads the entire file into a string using the specified charset.
148	 * @throw GdxRuntimeException if the file handle represents a directory, doesn't exist, or could not be read. */
149	public String readString (String charset) {
150		if (preloader.isText(file)) return preloader.texts.get(file);
151		try {
152			return new String(readBytes(), "UTF-8");
153		} catch (UnsupportedEncodingException e) {
154			return null;
155		}
156	}
157
158	/** Reads the entire file into a byte array.
159	 * @throw GdxRuntimeException if the file handle represents a directory, doesn't exist, or could not be read. */
160	public byte[] readBytes () {
161		int length = (int)length();
162		if (length == 0) length = 512;
163		byte[] buffer = new byte[length];
164		int position = 0;
165		InputStream input = read();
166		try {
167			while (true) {
168				int count = input.read(buffer, position, buffer.length - position);
169				if (count == -1) break;
170				position += count;
171				if (position == buffer.length) {
172					// Grow buffer.
173					byte[] newBuffer = new byte[buffer.length * 2];
174					System.arraycopy(buffer, 0, newBuffer, 0, position);
175					buffer = newBuffer;
176				}
177			}
178		} catch (IOException ex) {
179			throw new GdxRuntimeException("Error reading file: " + this, ex);
180		} finally {
181			try {
182				if (input != null) input.close();
183			} catch (IOException ignored) {
184			}
185		}
186		if (position < buffer.length) {
187			// Shrink buffer.
188			byte[] newBuffer = new byte[position];
189			System.arraycopy(buffer, 0, newBuffer, 0, position);
190			buffer = newBuffer;
191		}
192		return buffer;
193	}
194
195	/** Reads the entire file into the byte array. The byte array must be big enough to hold the file's data.
196	 * @param bytes the array to load the file into
197	 * @param offset the offset to start writing bytes
198	 * @param size the number of bytes to read, see {@link #length()}
199	 * @return the number of read bytes */
200	public int readBytes (byte[] bytes, int offset, int size) {
201		InputStream input = read();
202		int position = 0;
203		try {
204			while (true) {
205				int count = input.read(bytes, offset + position, size - position);
206				if (count <= 0) break;
207				position += count;
208			}
209		} catch (IOException ex) {
210			throw new GdxRuntimeException("Error reading file: " + this, ex);
211		} finally {
212			try {
213				if (input != null) input.close();
214			} catch (IOException ignored) {
215			}
216		}
217		return position - offset;
218	}
219
220	/** Returns a stream for writing to this file. Parent directories will be created if necessary.
221	 * @param append If false, this file will be overwritten if it exists, otherwise it will be appended.
222	 * @throw GdxRuntimeException if this file handle represents a directory, if it is a {@link FileType#Classpath} or
223	 *        {@link FileType#Internal} file, or if it could not be written. */
224	public OutputStream write (boolean append) {
225		throw new GdxRuntimeException("Cannot write to files in GWT backend");
226	}
227
228	/** Reads the remaining bytes from the specified stream and writes them to this file. The stream is closed. Parent directories
229	 * will be created if necessary.
230	 * @param append If false, this file will be overwritten if it exists, otherwise it will be appended.
231	 * @throw GdxRuntimeException if this file handle represents a directory, if it is a {@link FileType#Classpath} or
232	 *        {@link FileType#Internal} file, or if it could not be written. */
233	public void write (InputStream input, boolean append) {
234		throw new GdxRuntimeException("Cannot write to files in GWT backend");
235	}
236
237	/** Returns a writer for writing to this file using the default charset. Parent directories will be created if necessary.
238	 * @param append If false, this file will be overwritten if it exists, otherwise it will be appended.
239	 * @throw GdxRuntimeException if this file handle represents a directory, if it is a {@link FileType#Classpath} or
240	 *        {@link FileType#Internal} file, or if it could not be written. */
241	public Writer writer (boolean append) {
242		return writer(append, null);
243	}
244
245	/** Returns a writer for writing to this file. Parent directories will be created if necessary.
246	 * @param append If false, this file will be overwritten if it exists, otherwise it will be appended.
247	 * @param charset May be null to use the default charset.
248	 * @throw GdxRuntimeException if this file handle represents a directory, if it is a {@link FileType#Classpath} or
249	 *        {@link FileType#Internal} file, or if it could not be written. */
250	public Writer writer (boolean append, String charset) {
251		throw new GdxRuntimeException("Cannot write to files in GWT backend");
252	}
253
254	/** Writes the specified string to the file using the default charset. Parent directories will be created if necessary.
255	 * @param append If false, this file will be overwritten if it exists, otherwise it will be appended.
256	 * @throw GdxRuntimeException if this file handle represents a directory, if it is a {@link FileType#Classpath} or
257	 *        {@link FileType#Internal} file, or if it could not be written. */
258	public void writeString (String string, boolean append) {
259		writeString(string, append, null);
260	}
261
262	/** Writes the specified string to the file as UTF-8. Parent directories will be created if necessary.
263	 * @param append If false, this file will be overwritten if it exists, otherwise it will be appended.
264	 * @param charset May be null to use the default charset.
265	 * @throw GdxRuntimeException if this file handle represents a directory, if it is a {@link FileType#Classpath} or
266	 *        {@link FileType#Internal} file, or if it could not be written. */
267	public void writeString (String string, boolean append, String charset) {
268		throw new GdxRuntimeException("Cannot write to files in GWT backend");
269	}
270
271	/** Writes the specified bytes to the file. Parent directories will be created if necessary.
272	 * @param append If false, this file will be overwritten if it exists, otherwise it will be appended.
273	 * @throw GdxRuntimeException if this file handle represents a directory, if it is a {@link FileType#Classpath} or
274	 *        {@link FileType#Internal} file, or if it could not be written. */
275	public void writeBytes (byte[] bytes, boolean append) {
276		throw new GdxRuntimeException("Cannot write to files in GWT backend");
277	}
278
279	/** Writes the specified bytes to the file. Parent directories will be created if necessary.
280	 * @param append If false, this file will be overwritten if it exists, otherwise it will be appended.
281	 * @throw GdxRuntimeException if this file handle represents a directory, if it is a {@link FileType#Classpath} or
282	 *        {@link FileType#Internal} file, or if it could not be written. */
283	public void writeBytes (byte[] bytes, int offset, int length, boolean append) {
284		throw new GdxRuntimeException("Cannot write to files in GWT backend");
285	}
286
287	/** Returns the paths to the children of this directory. Returns an empty list if this file handle represents a file and not a
288	 * directory. On the desktop, an {@link FileType#Internal} handle to a directory on the classpath will return a zero length
289	 * array.
290	 * @throw GdxRuntimeException if this file is an {@link FileType#Classpath} file. */
291	public FileHandle[] list () {
292		return preloader.list(file);
293	}
294
295	/** Returns the paths to the children of this directory that satisfy the specified filter. Returns an empty list if this file
296	 * handle represents a file and not a directory. On the desktop, an {@link FileType#Internal} handle to a directory on the
297	 * classpath will return a zero length array.
298	 * @throw GdxRuntimeException if this file is an {@link FileType#Classpath} file. */
299	public FileHandle[] list (FileFilter filter) {
300		return preloader.list(file, filter);
301	}
302
303	/** Returns the paths to the children of this directory that satisfy the specified filter. Returns an empty list if this file
304	 * handle represents a file and not a directory. On the desktop, an {@link FileType#Internal} handle to a directory on the
305	 * classpath will return a zero length array.
306	 * @throw GdxRuntimeException if this file is an {@link FileType#Classpath} file. */
307	public FileHandle[] list (FilenameFilter filter) {
308		return preloader.list(file, filter);
309	}
310
311	/** Returns the paths to the children of this directory with the specified suffix. Returns an empty list if this file handle
312	 * represents a file and not a directory. On the desktop, an {@link FileType#Internal} handle to a directory on the classpath
313	 * will return a zero length array.
314	 * @throw GdxRuntimeException if this file is an {@link FileType#Classpath} file. */
315	public FileHandle[] list (String suffix) {
316		return preloader.list(file, suffix);
317	}
318
319	/** Returns true if this file is a directory. Always returns false for classpath files. On Android, an {@link FileType#Internal}
320	 * handle to an empty directory will return false. On the desktop, an {@link FileType#Internal} handle to a directory on the
321	 * classpath will return false. */
322	public boolean isDirectory () {
323		return preloader.isDirectory(file);
324	}
325
326	/** Returns a handle to the child with the specified name.
327	 * @throw GdxRuntimeException if this file handle is a {@link FileType#Classpath} or {@link FileType#Internal} and the child
328	 *        doesn't exist. */
329	public FileHandle child (String name) {
330		return new GwtFileHandle(preloader, (file.isEmpty() ? "" : (file + (file.endsWith("/") ? "" : "/"))) + name,
331			FileType.Internal);
332	}
333
334	public FileHandle parent () {
335		int index = file.lastIndexOf("/");
336		String dir = "";
337		if (index > 0) dir = file.substring(0, index);
338		return new GwtFileHandle(preloader, dir, type);
339	}
340
341	public FileHandle sibling (String name) {
342		return parent().child(fixSlashes(name));
343	}
344
345	/** @throw GdxRuntimeException if this file handle is a {@link FileType#Classpath} or {@link FileType#Internal} file. */
346	public void mkdirs () {
347		throw new GdxRuntimeException("Cannot mkdirs with an internal file: " + file);
348	}
349
350	/** Returns true if the file exists. On Android, a {@link FileType#Classpath} or {@link FileType#Internal} handle to a directory
351	 * will always return false. */
352	public boolean exists () {
353		return preloader.contains(file);
354	}
355
356	/** Deletes this file or empty directory and returns success. Will not delete a directory that has children.
357	 * @throw GdxRuntimeException if this file handle is a {@link FileType#Classpath} or {@link FileType#Internal} file. */
358	public boolean delete () {
359		throw new GdxRuntimeException("Cannot delete an internal file: " + file);
360	}
361
362	/** Deletes this file or directory and all children, recursively.
363	 * @throw GdxRuntimeException if this file handle is a {@link FileType#Classpath} or {@link FileType#Internal} file. */
364	public boolean deleteDirectory () {
365		throw new GdxRuntimeException("Cannot delete an internal file: " + file);
366	}
367
368	/** Copies this file or directory to the specified file or directory. If this handle is a file, then 1) if the destination is a
369	 * file, it is overwritten, or 2) if the destination is a directory, this file is copied into it, or 3) if the destination
370	 * doesn't exist, {@link #mkdirs()} is called on the destination's parent and this file is copied into it with a new name. If
371	 * this handle is a directory, then 1) if the destination is a file, GdxRuntimeException is thrown, or 2) if the destination is
372	 * a directory, this directory is copied into it recursively, overwriting existing files, or 3) if the destination doesn't
373	 * exist, {@link #mkdirs()} is called on the destination and this directory is copied into it recursively.
374	 * @throw GdxRuntimeException if the destination file handle is a {@link FileType#Classpath} or {@link FileType#Internal} file,
375	 *        or copying failed. */
376	public void copyTo (FileHandle dest) {
377		throw new GdxRuntimeException("Cannot copy to an internal file: " + dest);
378	}
379
380	/** Moves this file to the specified file, overwriting the file if it already exists.
381	 * @throw GdxRuntimeException if the source or destination file handle is a {@link FileType#Classpath} or
382	 *        {@link FileType#Internal} file. */
383	public void moveTo (FileHandle dest) {
384		throw new GdxRuntimeException("Cannot move an internal file: " + file);
385	}
386
387	/** 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
388	 * determined. */
389	public long length () {
390		return preloader.length(file);
391	}
392
393	/** Returns the last modified time in milliseconds for this file. Zero is returned if the file doesn't exist. Zero is returned
394	 * for {@link FileType#Classpath} files. On Android, zero is returned for {@link FileType#Internal} files. On the desktop, zero
395	 * is returned for {@link FileType#Internal} files on the classpath. */
396	public long lastModified () {
397		return 0;
398	}
399
400	public String toString () {
401		return file;
402	}
403
404	private static String fixSlashes(String path) {
405		path = path.replace('\\', '/');
406		if (path.endsWith("/")) {
407			path = path.substring(0, path.length() - 1);
408		}
409		return path;
410	}
411
412}
413