package org.basex.io; import java.io.BufferedInputStream; import java.io.DataInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.util.Locale; import java.util.regex.Pattern; import java.util.zip.GZIPInputStream; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; import org.basex.core.Prop; import org.basex.io.in.BufferInput; import org.basex.io.out.BufferOutput; import org.basex.util.Performance; import org.basex.util.TokenBuilder; import org.basex.util.Util; import org.basex.util.list.ByteList; import org.basex.util.list.ObjList; import org.basex.util.list.StringList; import org.xml.sax.InputSource; /** * {@link IO} reference, representing a local file or directory path. * * @author BaseX Team 2005-12, BSD License * @author Christian Gruen */ public final class IOFile extends IO { /** File reference. */ private final File file; /** Input stream reference to archived contents. */ private InputStream is; /** Expected size of a stream input. */ private long isSize = -1; /** Zip entry. */ ZipEntry zip; /** * Constructor. * @param f file path */ public IOFile(final String f) { this(new File(f)); } /** * Constructor. * @param f file reference */ public IOFile(final File f) { super(new PathList().create(f.getAbsolutePath())); file = f; } /** * Constructor. * @param dir directory * @param n file name */ public IOFile(final String dir, final String n) { this(new File(dir, n)); } /** * Constructor. * @param dir directory * @param n file name */ public IOFile(final File dir, final String n) { this(new File(dir, n)); } /** * Constructor. * @param dir directory * @param n file name */ public IOFile(final IOFile dir, final String n) { this(new File(dir.file, n)); } /** * Returns the file reference. * @return file reference */ public File file() { return file; } /** * Creates a new instance of this file. * @return success flag */ public boolean touch() { // some file systems require several runs for(int i = 0; i < 10; i++) { try { if(file.createNewFile()) return true; } catch(final IOException ex) { Performance.sleep(i * 10); Util.debug(ex); } } return false; } @Override public byte[] read() throws IOException { final long l = length(); if(l > -1) { // read all bytes in one go if length is known final DataInputStream dis = new DataInputStream( is == null ? new FileInputStream(file) : is); final byte[] cont = new byte[(int) l]; try { dis.readFully(cont); } finally { if(is == null) dis.close(); } return cont; } // otherwise, read from stream final BufferedInputStream bis = new BufferedInputStream(is); final ByteList bl = new ByteList(); for(int b; (b = bis.read()) != -1;) bl.add(b); return bl.toArray(); } @Override public boolean exists() { return file.exists(); } @Override public boolean isDir() { return file.isDirectory(); } @Override public long timeStamp() { return file.lastModified(); } @Override public long length() { return isSize != -1 ? isSize : file.length(); } @Override public boolean more(final boolean archives) throws IOException { if(archives) { if(is == null) { // process gzip files; assume input to be XML if(path.toLowerCase(Locale.ENGLISH).endsWith(GZSUFFIX)) { is = new GZIPInputStream(new FileInputStream(file)); init(name + XMLSUFFIX); isSize = -1; return true; } // process zip archives if(isArchive()) { is = new ZipInputStream(new FileInputStream(file)) { @Override public void close() throws IOException { if(zip == null) super.close(); } }; } } // check if stream returns more items if(is != null) { if(is instanceof ZipInputStream && moreZIP()) return true; is.close(); is = null; return false; } } // work on normal files return super.more(archives); } /** * Checks if a ZIP stream contains more entries. * @return result of check * @throws IOException I/O exception */ private boolean moreZIP() throws IOException { while(true) { zip = ((ZipInputStream) is).getNextEntry(); isSize = zip == null ? -1 : zip.getSize(); if(zip == null) return false; init(zip.getName()); if(!zip.isDirectory()) return true; } } @Override public boolean isArchive() { return isSuffix(ZIPSUFFIXES); } @Override public boolean isXML() { return isSuffix(XMLSUFFIXES); } /** * Tests if the file suffix matches the specified suffixed. * @param suffixes suffixes to compare with * @return result of check */ private boolean isSuffix(final String[] suffixes) { final int i = path.lastIndexOf('.'); if(i == -1) return false; final String suf = path.substring(i).toLowerCase(Locale.ENGLISH); for(final String z : suffixes) if(suf.equals(z)) return true; return false; } @Override public InputSource inputSource() { return is == null ? new InputSource(path) : new InputSource(is); } @Override public BufferInput buffer() throws IOException { // return file stream if(is == null) return new BufferInput(this); // return input stream final BufferInput in = new BufferInput(is); if(zip != null && zip.getSize() != -1) in.length(zip.getSize()); return in; } @Override public IO merge(final String f) { final IO io = IO.get(f); if(!(io instanceof IOFile)) return io; return f.contains(":") ? io : new IOFile(dir(), f); } /** * Recursively creates the directory. * @return contents */ public boolean md() { return !file.exists() && file.mkdirs(); } @Override public String dir() { return isDir() ? path : path.substring(0, path.lastIndexOf('/') + 1); } /** * Returns the children of the path. * @return children */ public IOFile[] children() { return children(".*"); } /** * Returns the children of the path that match the specified regular * expression. * @param pattern pattern * @return children */ public IOFile[] children(final String pattern) { final File[] ch = file.listFiles(); if(ch == null) return new IOFile[] {}; final ObjList<IOFile> io = new ObjList<IOFile>(ch.length); final Pattern p = Pattern.compile(pattern, Prop.WIN ? Pattern.CASE_INSENSITIVE : 0); for(final File f : ch) { if(p.matcher(f.getName()).matches()) io.add(new IOFile(f)); } return io.toArray(new IOFile[io.size()]); } /** * Returns the relative paths of all descendant files. * @return relative paths */ public synchronized StringList descendants() { final StringList files = new StringList(); final File[] ch = file.listFiles(); if(ch == null) return files; if(exists()) add(this, files, path().length() + 1); return files; } /** * Adds the relative paths of all descendant files to the specified list. * @param io current file * @param files file list * @param off string length of root path */ private static void add(final IOFile io, final StringList files, final int off) { if(io.isDir()) { for(final IOFile f : io.children()) add(f, files, off); } else { files.add(io.path().substring(off).replace('\\', '/')); } } /** * Writes the specified byte array. * @param c contents * @throws IOException I/O exception */ public void write(final byte[] c) throws IOException { final FileOutputStream out = new FileOutputStream(path); try { out.write(c); } finally { out.close(); } } /** * Writes the specified input. * @param in input stream * @throws IOException I/O exception */ public void write(final InputStream in) throws IOException { final BufferOutput out = new BufferOutput(path); try { for(int i; (i = in.read()) != -1;) out.write(i); } finally { try { in.close(); } catch(final IOException ex) { } out.close(); } } /** * Deletes the IO reference. * @return success flag */ public boolean delete() { boolean ok = true; if(isDir()) for(final IOFile ch : children()) ok &= ch.delete(); // some file systems require several runs for(int i = 0; i < 10; i++) { if(file.delete() && !file.exists()) return ok; Performance.sleep(i * 10); } return false; } /** * Renames the file to the specified name. * @param trg target reference * @return success flag */ public boolean rename(final IOFile trg) { return file.renameTo(trg.file); } @Override public String url() { final TokenBuilder tb = new TokenBuilder(FILEPREF); // add leading slash for Windows paths if(!path.startsWith("/")) tb.add("///"); for(int p = 0; p < path.length(); p++) { // replace spaces with %20 final char ch = path.charAt(p); if(ch == ' ') tb.add("%20"); else tb.add(ch); } return tb.toString(); } /** * Converts a file filter (glob) to a regular expression. * @param glob filter * @return regular expression */ public static String regex(final String glob) { return regex(glob, true); } /** * Checks if the specified string is a valid file reference. * @param s source * @return result of check */ static boolean valid(final String s) { if(s.length() < 3 || s.indexOf(':') == -1) return true; final char c = Character.toLowerCase(s.charAt(0)); return c >= 'a' && c <= 'z' && s.charAt(1) == ':'; } /** * Converts a file filter (glob) to a regular expression. A filter may * contain asterisks (*) and question marks (?); commas (,) are used to * separate multiple filters. * @param glob filter * @param sub accept substring in the result * @return regular expression */ public static String regex(final String glob, final boolean sub) { final StringBuilder sb = new StringBuilder(); for(final String g : glob.split(",")) { boolean suf = false; final String gl = g.trim(); if(sb.length() != 0) { if(!suf) sb.append(".*"); suf = false; sb.append('|'); } // loop through single pattern for(int f = 0; f < gl.length(); f++) { char ch = gl.charAt(f); if(ch == '*') { // don't allow other dots if pattern ends with a dot suf = true; sb.append(gl.endsWith(".") ? "[^.]" : "."); } else if(ch == '?') { ch = '.'; suf = true; } else if(ch == '.') { suf = true; // last character is dot: disallow file suffix if(f + 1 == gl.length()) break; sb.append('\\'); } else if(!Character.isLetterOrDigit(ch)) { sb.append('\\'); } sb.append(ch); } if(!suf && sub) sb.append(".*"); } return Prop.WIN ? sb.toString().toLowerCase(Locale.ENGLISH) : sb.toString(); } /** * Path constructor. Resolves parent and self references and normalizes the * path. */ static class PathList extends StringList { /** * Creates a path. * @param path input path * @return path */ String create(final String path) { final TokenBuilder tb = new TokenBuilder(); final int l = path.length(); for(int i = 0; i < l; ++i) { final char ch = path.charAt(i); if(ch == '\\' || ch == '/') add(tb); else tb.add(ch); } add(tb); if(path.startsWith("\\\\") || path.startsWith("//")) tb.add("//"); for(int s = 0; s < size; ++s) { if(s != 0 || path.startsWith("/")) tb.add('/'); tb.add(list[s]); } return tb.toString(); } /** * Adds a directory/file to the path list. * @param tb entry to be added */ private void add(final TokenBuilder tb) { String s = tb.toString(); // switch first Windows letter to upper case if(s.length() > 1 && s.charAt(1) == ':' && size == 0) { s = Character.toUpperCase(s.charAt(0)) + s.substring(1); } if(s.equals("..") && size > 0) { // parent step if(list[size - 1].indexOf(':') == -1) delete(size - 1); } else if(!s.equals(".") && !s.isEmpty()) { // skip self and empty steps add(s); } tb.reset(); } } }