package org.basex.io; import java.io.*; import java.nio.file.*; import java.util.*; import java.util.regex.*; import javax.xml.transform.stream.*; import org.basex.io.out.*; import org.basex.util.*; import org.basex.util.list.*; import org.xml.sax.*; /** * {@link IO} reference, representing a local file or directory path. * * @author BaseX Team 2005-17, BSD License * @author Christian Gruen */ public final class IOFile extends IO { /** Pattern for valid file names. */ private static final Pattern VALIDNAME = Pattern.compile("^[^\\\\/" + (Prop.WIN ? ":*?\"<>\\|" : "") + "]+$"); /** Absolute flag. */ private final boolean absolute; /** File reference. */ private final File file; /** * Constructor. * @param file file reference * @param last last path segment */ public IOFile(final File file, final String last) { super(create(file.getAbsolutePath(), last)); boolean abs = file.isAbsolute(); this.file = abs ? file : new File(pth); // Windows: checks if the original file path starts with a slash if(!abs && Prop.WIN) { final String p = file.getPath(); abs = p.startsWith("/") || p.startsWith("\\"); } absolute = abs; } /** * Constructor. * @param file file reference */ public IOFile(final File file) { this(file, ""); } /** * Constructor. * @param path file path */ public IOFile(final String path) { this(new File(path), path); } /** * Constructor. * @param dir parent directory string * @param child child directory string */ public IOFile(final String dir, final String child) { this(new File(dir, child), child); } /** * Constructor. * @param dir directory string * @param child child path string */ public IOFile(final IOFile dir, final String child) { this(new File(dir.file, child), child); } /** * 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() { try { Files.createFile(toPath()); return true; } catch(final IOException ex) { Util.debug(ex); return false; } } @Override public byte[] read() throws IOException { return Files.readAllBytes(toPath()); } @Override public boolean exists() { return file.exists(); } @Override public boolean isDir() { return file.isDirectory(); } @Override public boolean isAbsolute() { return absolute; } @Override public long timeStamp() { return file.lastModified(); } @Override public long length() { return file.length(); } @Override public InputSource inputSource() { return new InputSource(url()); } @Override public StreamSource streamSource() { return new StreamSource(pth); } @Override public InputStream inputStream() throws IOException { return new FileInputStream(file); } /** * Resolves two paths. * @param path file path (relative or absolute) * @return resulting path */ public IOFile resolve(final String path) { final IOFile f = new IOFile(path); return f.absolute ? f : new IOFile(isDir() ? pth : dir(), path); } /** * Recursively creates the directory if it does not exist yet. * @return {@code true} if the directory exists or has been created. */ public boolean md() { return file.exists() || file.mkdirs(); } /** * Returns the parent of this file or directory or {@code null} if there is no parent directory. * @return directory or {@code null} */ public IOFile parent() { final String parent = file.getParent(); return parent == null ? null : new IOFile(parent + '/'); } /** * 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 regex regular expression pattern * @return children */ public IOFile[] children(final String regex) { final File[] ch = file.listFiles(); if(ch == null) return new IOFile[0]; final ArrayList<IOFile> io = new ArrayList<>(ch.length); final Pattern p = Pattern.compile(regex, Prop.CASE ? 0 : Pattern.CASE_INSENSITIVE); 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 children of the path that match the specified filter. * @param filter file filter * @return children */ public IOFile[] children(final FileFilter filter) { final File[] ch = filter == null ? file.listFiles() : file.listFiles(filter); if(ch == null) return new IOFile[0]; final ArrayList<IOFile> io = new ArrayList<>(ch.length); for(final File f : ch) { io.add(new IOFile(f)); } return io.toArray(new IOFile[io.size()]); } /** * Returns the relative paths of all descendant files (excluding directories). * @return relative paths */ public StringList descendants() { return descendants(null); } /** * Returns the relative paths of all descendant non-filtered files (excluding directories). * @param filter file filter * @return relative paths */ public StringList descendants(final FileFilter filter) { final StringList files = new StringList(); final File[] ch = filter == null ? file.listFiles() : file.listFiles(filter); if(ch == null) return files; if(exists()) addDescendants(this, files, filter, path().length() + 1); return files; } /** * Writes the specified byte array. * @param bytes bytes * @throws IOException I/O exception */ public void write(final byte[] bytes) throws IOException { Files.write(toPath(), bytes); } /** * Writes the specified input. The specified input stream is eventually closed. * @param in input stream * @throws IOException I/O exception */ public void write(final InputStream in) throws IOException { try(BufferOutput out = new BufferOutput(pth)) { for(int i; (i = in.read()) != -1;) out.write(i); } } /** * Deletes the file, or the directory and its children. * @return {@code true} if the file does not exist or has been deleted. */ public boolean delete() { boolean ok = true; if(file.exists()) { if(isDir()) { for(final IOFile ch : children()) ok &= ch.delete(); } try { Files.delete(toPath()); } catch(final IOException ex) { Util.debug(ex); return false; } } return ok; } /** * Renames a file to the specified path. The path must not exist yet. * @param target target reference * @return success flag */ public boolean rename(final IOFile target) { return file.renameTo(target.file); } /** * Copies a file to another target. * @param target target * @throws IOException I/O exception */ public void copyTo(final IOFile target) throws IOException { // create parent directory of target file target.parent().md(); Files.copy(toPath(), target.toPath(), StandardCopyOption.REPLACE_EXISTING); } @Override public boolean eq(final IO io) { return io instanceof IOFile && (Prop.CASE ? pth.equals(io.pth) : pth.equalsIgnoreCase(io.pth)); } @Override public boolean equals(final Object o) { return o instanceof IOFile && ((IOFile) o).pth.equals(pth); } @Override public String url() { final TokenBuilder tb = new TokenBuilder(FILEPREF); String path = pth; if(path.startsWith("/")) { path = path.substring(1); } else { // add leading slash for Windows paths tb.add("//"); } final int pl = path.length(); for(int p = 0; p < pl; p++) { // replace spaces with %20 final char ch = path.charAt(p); if(ch == ' ') tb.add("%20"); else tb.add(ch); } return tb.toString(); } /** * Opens the file externally. * @throws IOException I/O exception */ public void open() throws IOException { final String[] args; if(Prop.WIN) { args = new String[] { "rundll32", "url.dll,FileProtocolHandler", pth }; } else if(Prop.MAC) { args = new String[] { "/usr/bin/open", pth }; } else { args = new String[] { "xdg-open", pth }; } new ProcessBuilder(args).directory(parent().file).start(); } /** * Returns a native file path representation. If normalization fails, returns the original path. * @return path */ public IOFile normalize() { try { return new IOFile(toPath().toRealPath().toFile()); } catch(final IOException ex) { return this; } } // STATIC METHODS =============================================================================== /** * Returns a {@link Path} instance of this file. * @return path * @throws IOException I/O exception */ private Path toPath() throws IOException { try { return Paths.get(pth); } catch(final InvalidPathException ex) { Util.debug(ex); throw new IOException(ex); } } /** * Adds the relative paths of all descendant files to the specified list. * @param io current file * @param files file list * @param filter file filter * @param offset string length of root path */ private static void addDescendants(final IOFile io, final StringList files, final FileFilter filter, final int offset) { if(io.isDir()) { for(final IOFile f : io.children(filter)) addDescendants(f, files, filter, offset); } else { if(filter == null || filter.accept(io.file)) { files.add(io.path().substring(offset)); } } } /** * Checks if the specified string is a valid file name. * @param name file name * @return result of check */ public static boolean isValidName(final String name) { return VALIDNAME.matcher(name).matches(); } /** * Checks if the specified string is a valid file reference. * @param path path string * @return result of check */ public static boolean isValid(final String path) { // no colon: treat as file path final int c = path.indexOf(':'); if(c == -1) return true; // Windows drive letter? final int fs = path.indexOf('/'), bs = path.indexOf('\\'); if(Prop.WIN && c == 1 && Token.letter(path.charAt(0)) && (fs == 2 || bs == 2)) return true; // ensure that slash occurs before colon return fs != -1 && fs < c || bs != -1 && bs < c; } /** * Converts a name filter (glob) to a regular expression. * @param glob filter * @return regular expression */ public static String regex(final String glob) { return regex(glob, true); } /** * 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 globs : Strings.split(glob, ',')) { final String glb = globs.trim(); if(sb.length() != 0) sb.append('|'); // loop through single pattern boolean suf = false; final int gl = glb.length(); for(int g = 0; g < gl; g++) { char ch = glb.charAt(g); if(ch == '*') { // don't allow other dots if pattern ends with a dot suf = true; sb.append(glb.endsWith(".") ? "[^.]" : "."); } else if(ch == '?') { ch = '.'; suf = true; } else if(ch == '.') { suf = true; // last character is dot: disallow file suffix if(g + 1 == glb.length()) break; sb.append('\\'); } else if(!Character.isLetterOrDigit(ch)) { sb.append('\\'); } sb.append(ch); } if(!suf && sub) sb.append(".*"); } return Prop.CASE ? sb.toString() : sb.toString().toLowerCase(Locale.ENGLISH); } // PRIVATE METHODS ============================================================================== /** * Creates a path. * @param path input path * @param last last segment * @return path */ private static String create(final String path, final String last) { final StringList sl = new StringList(); final int l = path.length(); final TokenBuilder tb = new TokenBuilder(l); for(int i = 0; i < l; ++i) { final char ch = path.charAt(i); if(ch == '\\' || ch == '/') add(tb, sl); else tb.add(ch); } add(tb, sl); if(path.startsWith("\\\\") || path.startsWith("//")) tb.add("//"); final int size = sl.size(); for(int s = 0; s < size; ++s) { if(s != 0 || path.startsWith("/")) tb.add('/'); tb.add(sl.get(s)); } // add slash if original file ends with a slash, or if path is a Windows root directory boolean dir = last.endsWith("/") || last.endsWith("\\"); if(!dir && Prop.WIN && tb.size() == 2) { final int c = Character.toLowerCase(tb.get(0)); dir = c >= 'a' && c <= 'z' && tb.get(1) == ':'; } if(dir) tb.add('/'); return tb.toString(); } /** * Adds a directory/file to the path list. * @param tb entry to be added * @param sl string list */ private static void add(final TokenBuilder tb, final StringList sl) { String s = tb.toString(); // switch first Windows letter to upper case if(s.length() > 1 && s.charAt(1) == ':' && sl.isEmpty()) { s = Character.toUpperCase(s.charAt(0)) + s.substring(1); } if("..".equals(s) && !sl.isEmpty()) { // parent step if(sl.get(sl.size() - 1).indexOf(':') == -1) sl.remove(sl.size() - 1); } else if(!".".equals(s) && !s.isEmpty()) { // skip self and empty steps sl.add(s); } tb.reset(); } }