001package org.unix4j.util;
002
003import java.io.File;
004import java.util.List;
005
006/**
007 * This class represents a base directory for relative paths. The
008 * {@link #getRelativePathFor(File)} method returns a path string relative to
009 * the base directory passed to the constructor of this class.
010 */
011public class RelativePathBase {
012
013        /**
014         * Settings used to construct relative paths.
015         */
016        public static interface Settings {
017                /**
018                 * Appends the path for a file that equals the base directory. Some
019                 * settings may append "." or "./" and others the base directory name.
020                 * 
021                 * @param result
022                 *            the string builder with the result path
023                 * @param baseDir
024                 *            the base directory, here also the relative path target
025                 * @return the {@code result} instance for chained invocations
026                 */
027                StringBuilder appendPathForBaseDir(StringBuilder result, File baseDir);
028
029                /**
030                 * Appends the path prefix for children of the base directory. Most
031                 * settings add "./" as prefix, others maybe nothing and some add
032                 * nothing for direct children but "./" for all other child paths.
033                 * 
034                 * @param result
035                 *            the string builder with the result path
036                 * @param baseDir
037                 *            the base directory, here a parent directory of the
038                 *            relative path target
039                 * @return the {@code result} instance for chained invocations
040                 */
041                StringBuilder appendPrefixForChildren(StringBuilder result, File baseDir, boolean directChildOfBase);
042
043                /**
044                 * Appends the path prefix from the base directory to the common
045                 * ancestor of base directory and target file. Most settings add one
046                 * "../" element for every directory up to the common ancestor.
047                 * 
048                 * @param result
049                 *            the string builder with the result path
050                 * @param baseDir
051                 *            the base directory, here a directory which has a common
052                 *            ancestor with the relative path target
053                 * @return the {@code result} instance for chained invocations
054                 */
055                StringBuilder appendPrefixToCommonAncestor(StringBuilder result, File baseDir, int levelsToCommonAncestor);
056        }
057
058        /**
059         * Default settings to construct relative paths: "." represents the base
060         * directory, "./" is used as prefix of child paths (but "" for direct
061         * children) and "../" for elements up to the common ancestor.
062         */
063        public static final Settings DEFAULT = new Settings() {
064                /**
065                 * Appends "." to {@code result} representing the base directory.
066                 */
067                @Override
068                public StringBuilder appendPathForBaseDir(StringBuilder result, File baseDir) {
069                        return result.append('.');
070                }
071
072                /**
073                 * Appends "./" to {@code result} as prefix for child directories, but
074                 * only if {@code directChildOfBase==false} (no prefix for direct
075                 * children of the base directory).
076                 */
077                @Override
078                public StringBuilder appendPrefixForChildren(StringBuilder result, File baseDir, boolean directChildOfBase) {
079                        if (directChildOfBase) {
080                                return result;
081                        }
082                        return result.append("./");
083                }
084
085                /**
086                 * Appends one "../" element for every directory up to the common
087                 * ancestor ({@code n} elements where {@code n=levelsToCommonAncestor}).
088                 */
089                @Override
090                public StringBuilder appendPrefixToCommonAncestor(StringBuilder result, File baseDir, int levelsToCommonAncestor) {
091                        for (int i = 0; i < levelsToCommonAncestor; i++) {
092                                result.append("../");
093                        }
094                        return result;
095                }
096        };
097        /**
098         * Alternative settings to construct relative paths similar to
099         * {@link #DEFAULT}: "." represents the base directory, "./" is used as
100         * prefix of child paths and "../" for elements up to the common ancestor.
101         * As opposed to the DEFAULT settings, the "./" prefix is used for all child
102         * paths including direct children.
103         */
104        public static final Settings ALL_CHILDREN_WITH_DOT_PREFIX = new Settings() {
105                /**
106                 * Appends "." to {@code result} representing the base directory.
107                 */
108                @Override
109                public StringBuilder appendPathForBaseDir(StringBuilder result, File baseDir) {
110                        return result.append('.');
111                }
112
113                /**
114                 * Appends "./" to {@code result} as prefix for child directories, but
115                 * only if {@code directChildOfBase==false} (no prefix for direct
116                 * children of the base directory).
117                 */
118                @Override
119                public StringBuilder appendPrefixForChildren(StringBuilder result, File baseDir, boolean directChildOfBase) {
120                        return result.append("./");
121                }
122
123                /**
124                 * Appends one "../" element for every directory up to the common
125                 * ancestor ({@code n} elements where {@code n=levelsToCommonAncestor}).
126                 */
127                @Override
128                public StringBuilder appendPrefixToCommonAncestor(StringBuilder result, File baseDir, int levelsToCommonAncestor) {
129                        for (int i = 0; i < levelsToCommonAncestor; i++) {
130                                result.append("../");
131                        }
132                        return result;
133                }
134        };
135        /**
136         * Alternative settings to construct relative paths without prefixes for
137         * children: "." represents the base directory, no prefix for child paths
138         * and "../" for elements up to the common ancestor.
139         */
140        public static final Settings CHILDREN_WITHOUT_PREFIX = new Settings() {
141                /**
142                 * Appends "." to {@code result} representing the base directory.
143                 */
144                @Override
145                public StringBuilder appendPathForBaseDir(StringBuilder result, File baseDir) {
146                        return result.append('.');
147                }
148
149                /**
150                 * Appends "./" to {@code result} as prefix for child directories, but
151                 * only if {@code directChildOfBase==false} (no prefix for direct
152                 * children of the base directory).
153                 */
154                @Override
155                public StringBuilder appendPrefixForChildren(StringBuilder result, File baseDir, boolean directChildOfBase) {
156                        return result;
157                }
158
159                /**
160                 * Appends one "../" element for every directory up to the common
161                 * ancestor ({@code n} elements where {@code n=levelsToCommonAncestor}).
162                 */
163                @Override
164                public StringBuilder appendPrefixToCommonAncestor(StringBuilder result, File baseDir, int levelsToCommonAncestor) {
165                        for (int i = 0; i < levelsToCommonAncestor; i++) {
166                                result.append("../");
167                        }
168                        return result;
169                }
170        };
171
172        private final File base;
173        private final Settings settings;
174
175        /**
176         * Constructor with base directory for relative paths using {@link #DEFAULT}
177         * settings to construct relative paths.
178         * 
179         * @param base
180         *            the basis for relative paths returned by
181         *            {@link #getRelativePathFor(File)}
182         */
183        public RelativePathBase(String base) {
184                this(new File(base));
185        }
186
187        /**
188         * Constructor with base directory for relative paths using {@link #DEFAULT}
189         * settings to construct relative paths.
190         * 
191         * @param base
192         *            the basis for relative paths returned by
193         *            {@link #getRelativePathFor(File)}
194         */
195        public RelativePathBase(File base) {
196                this(base, DEFAULT);
197        }
198
199        /**
200         * Constructor with base directory for relative paths using the specified
201         * settings to construct relative paths.
202         * 
203         * @param base
204         *            the basis for relative paths returned by
205         *            {@link #getRelativePathFor(File)}
206         * @param settings
207         *            the settings used to construct relative paths
208         */
209        public RelativePathBase(String base, Settings settings) {
210                this(new File(base), settings);
211        }
212
213        /**
214         * Constructor with base directory for relative paths using the specified
215         * settings to construct relative paths.
216         * 
217         * @param base
218         *            the basis for relative paths returned by
219         *            {@link #getRelativePathFor(File)}
220         * @param settings
221         *            the settings used to construct relative paths
222         */
223        public RelativePathBase(File base, Settings settings) {
224                this.base = base;
225                this.settings = settings;
226        }
227
228        /**
229         * Returns the base directory for relative paths, the directory that was
230         * passed to the constructor.
231         */
232        public File getBase() {
233                return base;
234        }
235
236        /**
237         * Returns the settings used to construct relative paths. Has been passed to
238         * the constructor unless {@link #DEFAULT} settings are used.
239         * 
240         * @return the settings used to construct relative paths
241         */
242        public Settings getSettings() {
243                return settings;
244        }
245
246        /**
247         * Returns the path of the given {@code file} relative to the
248         * {@link #getBase() base} directory.
249         * <p>
250         * The relative path is evaluated as follows:
251         * <ol>
252         * <li>If {@code base} is the same as {@code file}, "." is returned</li>
253         * <li>If {@code base} is the direct parent directory of {@code file}, the
254         * simple file name is returned</li>
255         * <li>If {@code base} is a non-direct parent directory of {@code file}, the
256         * relative path from {@code base} to {@code file} is returned</li>
257         * <li>If {@code base} is not parent directory of {@code file}, the relative
258         * path is the path from {@code base} to the common ancestor and then to
259         * {@code file}</li>
260         * <li>If {@code base} is not parent directory of {@code file} and the only
261         * common ancestor of the two is the base directory, the absolute path of
262         * {@code file} is returned</li>
263         * </ol>
264         * Examples (same order as above):
265         * <ol>
266         * <li>("/home/john", "/home/john") &rarr; "."</li>
267         * <li>("/home/john", "/home/john/notes.txt") &rarr; "notes.txt"</li>
268         * <li>("/home/john", "/home/john/documents/important") &rarr;
269         * "./documents/important"</li>
270         * <li>("/home/john", "/home/smith/public/holidays.pdf") &rarr;
271         * "../smith/public/holidays.pdf"</li>
272         * <li>("/home/john", "/var/tmp/test.out") &rarr; "/var/tmp/test.out"</li>
273         * </ol>
274         * 
275         * @param file
276         *            the target file whose relative path is returned
277         * @return the path of {@code file} relative to the {@link #getBase() base}
278         *         directory
279         */
280        public String getRelativePathFor(File file) {
281                final StringBuilder path = new StringBuilder();
282                if (base.equals(file)) {
283                        return settings.appendPathForBaseDir(path, file).toString();
284                }
285                if (base.equals(file.getParentFile())) {
286                        return settings.appendPrefixForChildren(path, base, true).append(file.getName()).toString();
287                }
288                final List<String> baseParts = FileUtil.getPathElements(base);
289                final List<String> fileParts = FileUtil.getPathElements(file);
290
291                final int len = Math.min(baseParts.size(), fileParts.size());
292                int common = 0;
293                while (common < len && baseParts.get(common).equals(fileParts.get(common))) {
294                        common++;
295                }
296                if (common == 0) {
297                        return file.getAbsolutePath().replace('\\', '/');
298                }
299                if (common < baseParts.size()) {
300                        settings.appendPrefixToCommonAncestor(path, base, baseParts.size() - common);
301                } else {
302                        settings.appendPrefixForChildren(path, base, false);
303                }
304                for (int i = common; i < fileParts.size(); i++) {
305                        path.append(fileParts.get(i)).append('/');
306                }
307                // cut off last '/' and return string
308                return path.deleteCharAt(path.length() - 1).toString();
309        }
310
311        /**
312         * Returns the path of the given {@code file} relative to the
313         * {@link #getBase() base} directory.
314         * <p>
315         * The relative path is evaluated as follows:
316         * <ol>
317         * <li>If {@code base} is the same as {@code file}, "." is returned</li>
318         * <li>If {@code base} is the direct parent directory of {@code file}, the
319         * simple file name is returned</li>
320         * <li>If {@code base} is a non-direct parent directory of {@code file}, the
321         * relative path from {@code base} to {@code file} is returned</li>
322         * <li>If {@code base} is not parent directory of {@code file}, the relative
323         * path is the path from {@code base} to the common ancestor and then to
324         * {@code file}</li>
325         * <li>If {@code base} is not parent directory of {@code file} and the only
326         * common ancestor of the two is the base directory, the absolute path of
327         * {@code file} is returned</li>
328         * </ol>
329         * Examples (same order as above):
330         * <ol>
331         * <li>("/home/john", "/home/john") &rarr; "."</li>
332         * <li>("/home/john", "/home/john/notes.txt") &rarr; "notes.txt"</li>
333         * <li>("/home/john", "/home/john/documents/important") &rarr;
334         * "./documents/important"</li>
335         * <li>("/home/john", "/home/smith/public/holidays.pdf") &rarr;
336         * "../smith/public/holidays.pdf"</li>
337         * <li>("/home/john", "/var/tmp/test.out") &rarr; "/var/tmp/test.out"</li>
338         * </ol>
339         * 
340         * @param file
341         *            the target file whose relative path is returned
342         * @return the path of {@code file} relative to the {@link #getBase() base}
343         *         directory
344         */
345        public String getRelativePathFor(String file) {
346                return getRelativePathFor(new File(file));
347        }
348}