package ch.cyberduck.core; /* * Copyright (c) 2007 David Kocher. All rights reserved. * http://cyberduck.ch/ * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * Bug fixes, suggestions and comments should be sent to: * dkocher@cyberduck.ch */ import org.apache.commons.io.FilenameUtils; import org.apache.commons.lang.StringUtils; import org.apache.log4j.Logger; //import org.jets3t.service.utils.Mimetypes; //import com.ibm.icu.text.Normalizer; import java.util.Comparator; /** * @version $Id$ */ public abstract class AbstractPath { private static Logger log = Logger.getLogger(AbstractPath.class); /** * The path delimiter */ public static final String DELIMITER = "/"; /** * Shortcut for the home directory */ public static final String HOME = "~"; /** * Attributes denoting this path * @uml.property name="attributes" * @uml.associationEnd */ public Attributes attributes; public static final int FILE_TYPE = 1; public static final int DIRECTORY_TYPE = 2; public static final int SYMBOLIC_LINK_TYPE = 4; public static final int VOLUME_TYPE = 8; /** * @return True if this path denotes a directory and its file listing is cached for this session * @see ch.cyberduck.core.Cache */ public boolean isCached() { return this.cache().containsKey(this) && !this.cache().get(this).attributes().isDirty(); } public abstract <T extends AbstractPath> Cache<T> cache(); /** * Clear cached listing * * @throws NullPointerException if session is not initialized */ public void invalidate() { if(attributes.isDirectory()) { this.cache().get(this).attributes().setDirty(true); } } public abstract <T> PathReference<T> getReference(); public abstract String toURL(); /** * Fetch the directory listing * * @return */ public abstract <T extends AbstractPath> AttributedList<T> list(); /** * Get the cached directory listing if any or return #list instead. * No sorting and filtering applied. * * @return Cached directory listing as returned by the server * @see #list() */ public <T extends AbstractPath> AttributedList<T> childs() { return this.childs(new NullPathFilter<T>()); } /** * Get the cached directory listing if any or return #list instead. * No sorting applied. * * @param filter Filter to apply to directory listing * @param <T> * @return Cached directory listing as returned by the server filtered * @see #list() */ public <T extends AbstractPath> AttributedList<T> childs(PathFilter<T> filter) { return this.childs(new NullComparator<T>(), filter); } /** * Request a sorted and filtered file listing from the server. Has to be a directory. * A cached listing is returned if possible * * @param comparator The comparator to sort the listing with * @param filter The filter to exlude certain files * @return The children of this path or an empty list if it is not accessible for some reason * @see #list() */ public <T extends AbstractPath> AttributedList<T> childs(Comparator<T> comparator, PathFilter<T> filter) { if(!this.isCached()) { this.cache().put(this, this.list()); } return this.<T>cache().get((T) this, comparator, filter); } /** * @return true if this paths points to '/' */ public boolean isRoot() { return this.getAbsolute().equals(DELIMITER); } public static String getParent(String absolute) { int index = absolute.length() - 1; if(absolute.charAt(index) == '/') { if(index > 0) { index--; } } int cut = absolute.lastIndexOf('/', index); if(cut > 0) { return absolute.substring(0, cut); } //if (index == 0) //parent is root return DELIMITER; } /** * @uml.property name="absolute" */ public abstract String getAbsolute(); /** * @uml.property name="name" */ public abstract String getName(); /** * Subclasses may override to return a user friendly representation of the name denoting this path. */ public String getDisplayName() { return this.getName(); } /** * @uml.property name="parent" * @uml.associationEnd readOnly="true" */ public abstract AbstractPath getParent(); public abstract boolean exists(); public static String normalize(final String path) { return normalize(path, true); } /** * Return a context-relative path, beginning with a "/", that represents * the canonical version of the specified path after ".." and "." elements * are resolved out. * * @param path The path to parse * @param absolute If the path is absolute * @return the normalized path. * @author Adapted from org.apache.webdav * @license http://www.apache.org/licenses/LICENSE-2.0 */ public static String normalize(final String path, final boolean absolute) { if(null == path) { return DELIMITER; } String normalized = path; if(Preferences.instance().getBoolean("path.normalize")) { if(absolute) { while(!normalized.startsWith("\\\\") && !normalized.startsWith(DELIMITER)) { normalized = DELIMITER + normalized; } } while(!normalized.endsWith(DELIMITER)) { normalized += DELIMITER; } // Resolve occurrences of "/./" in the normalized path while(true) { int index = normalized.indexOf("/./"); if(index < 0) { break; } normalized = normalized.substring(0, index) + normalized.substring(index + 2); } // Resolve occurrences of "/../" in the normalized path while(true) { int index = normalized.indexOf("/../"); if(index < 0) { break; } if(index == 0) { return DELIMITER; // The only left path is the root. } normalized = normalized.substring(0, normalized.lastIndexOf('/', index - 1)) + normalized.substring(index + 3); } StringBuilder n = new StringBuilder(); if(normalized.startsWith("//")) { // see #972. Omit leading delimiter n.append(DELIMITER); n.append(DELIMITER); } else if(normalized.startsWith("\\\\")) { ; } else if(absolute) { // convert to absolute path n.append(DELIMITER); } else if(normalized.startsWith(DELIMITER)) { // Keep absolute path n.append(DELIMITER); } // Remove duplicated delimiters String[] segments = normalized.split(Path.DELIMITER); for(String segment : segments) { if(segment.equals("")) { continue; } n.append(segment); n.append(DELIMITER); } normalized = n.toString(); while(normalized.endsWith(DELIMITER) && normalized.length() > 1) { //Strip any redundant delimiter at the end of the path normalized = normalized.substring(0, normalized.length() - 1); } } if(Preferences.instance().getBoolean("path.normalize.unicode")) { // if(!Normalizer.isNormalized(normalized, Normalizer.NFC, Normalizer.UNICODE_3_2)) { // normalized = Normalizer.normalize(normalized, Normalizer.NFC, Normalizer.UNICODE_3_2); // } } // Return the normalized path that we have completed return normalized; } /** * @return the extension if any or null otherwise */ public String getExtension() { final String extension = FilenameUtils.getExtension(this.getName()); if(StringUtils.isEmpty(extension)) { return null; } return extension; } public String getMimeType() { return getMimeType(this.getName().toLowerCase()); } public static String getMimeType(String extension) { // return Mimetypes.getInstance().getMimetype(extension); return ""; } /** * @param parent The parent directory * @param name The relative filename */ public void setPath(String parent, String name) { if(null == parent) { parent = DELIMITER; } //Determine if the parent path already ends with a delimiter if(parent.endsWith(DELIMITER)) { this.setPath(parent + name); } else { this.setPath(parent + DELIMITER + name); } } public abstract void setPath(String name); /** * @param p * @return true if p is a child of me in the path hierarchy */ public boolean isChild(AbstractPath p) { for(AbstractPath parent = this.getParent(); !parent.isRoot(); parent = parent.getParent()) { if(parent.equals(p)) { return true; } } return false; } /** * @param parent Absolute path to the symbolic link * @param name Target of the symbolic link name. Absolute or relative pathname */ public void setSymlinkTarget(String parent, String name) { if(name.startsWith(DELIMITER)) { // Symbolic link target may be an absolute path this.setSymlinkTarget(name); } else { if(parent.endsWith(DELIMITER)) { this.setSymlinkTarget(parent + name); } else { this.setSymlinkTarget(parent + DELIMITER + name); } } } /** * An absolute reference here the symbolic link is pointing to * @uml.property name="symbolic" */ private String symbolic = null; public void setSymlinkTarget(String p) { this.symbolic = AbstractPath.normalize(p); } /** * @return The target of the symbolic link if this path denotes a symbolic link * @see ch.cyberduck.core.PathAttributes#isSymbolicLink */ public String getSymlinkTarget() { if(this.attributes.isSymbolicLink()) { return this.symbolic; } return null; } public boolean isMkdirSupported() { return true; } public void mkdir() { this.mkdir(false); } /** * @param recursive Create intermediate directories as required. If this option is * not specified, the full path prefix of each operand must already exist */ public abstract void mkdir(boolean recursive); public boolean isWritePermissionsSupported() { return true; } /** * @param perm The permissions to apply * @param recursive Include subdirectories and files */ public abstract void writePermissions(Permission perm, boolean recursive); public boolean isWriteModificationDateSupported() { return true; } /** * @param millis Milliseconds since 1970 */ public abstract void writeModificationDate(long millis); /** * Remove this file from the remote host. Does not affect any corresponding local file */ public abstract void delete(); /** * @return True if the directory contains no childs items */ public boolean isEmpty() { return this.childs().size() == 0; } public boolean isRenameSupported() { return true; } /** * @param renamed Must be an absolute path */ public abstract void rename(AbstractPath renamed); /** * @param copy */ public abstract void copy(AbstractPath copy); /** * @return true if executable for user, group and world */ public boolean isExecutable() { final Permission perm = attributes.getPermission(); if(null == perm) { log.warn("Unknown permissions"); return true; } return perm.getOwnerPermissions()[Permission.EXECUTE] || perm.getGroupPermissions()[Permission.EXECUTE] || perm.getOtherPermissions()[Permission.EXECUTE]; } /** * @return true if readable for user, group and world */ public boolean isReadable() { final Permission perm = attributes.getPermission(); if(null == perm) { log.warn("Unknown permissions"); return true; } return perm.getOwnerPermissions()[Permission.READ] || perm.getGroupPermissions()[Permission.READ] || perm.getOtherPermissions()[Permission.READ]; } /** * @return true if writable for user, group and world */ public boolean isWritable() { final Permission perm = attributes.getPermission(); if(null == perm) { log.warn("Unknown permissions"); return true; } return perm.getOwnerPermissions()[Permission.WRITE] || perm.getGroupPermissions()[Permission.WRITE] || perm.getOtherPermissions()[Permission.WRITE]; } /** * Calculate the MD5 sum as Hex-encoded string or null if failure */ public abstract void readChecksum(); }