/******************************************************************************* * Copyright (c) 2011 Arapiki Solutions Inc. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * "Peter Smith <psmith@arapiki.com>" - initial API and * implementation and/or initial documentation *******************************************************************************/ package com.buildml.utils.string; import java.util.ArrayList; import java.util.EmptyStackException; import java.util.List; import java.util.Stack; /** * This class provides various utility functions for manipulating file system * paths, and their path components. All the methods in this class are static, * so they're essentially just worker functions without any state. * * @author "Peter Smith <psmith@arapiki.com>" */ public class PathUtils { /*-------------------------------------------------------------------------------------*/ /** * Given a single component of a file system path, validate that it's well formed. This * ensures that the name doesn't contain / or \, and that it's not empty, . or .. * * @param pathComponent The path component to validate. * @return True if name is valid, else false. */ public static boolean validatePathComponent(String pathComponent) { return !((pathComponent == null) || (pathComponent.isEmpty()) || (pathComponent.contains("/")) || (pathComponent.contains("\\")) || (pathComponent.equals(".")) || (pathComponent.equals(".."))); } /*-------------------------------------------------------------------------------------*/ /** * Given an absolute path (starting with '/'), normalize it so that there * are no ".." or "." components, and no excess / characters. Note that unlike * File.getCanonicalPath(), we do not access the underlying file system * (this allows us to process content on a different machine from which the * path actually exists). * * @param curDir The non-normalized path. * @return The normalized path. */ public static String normalizeAbsolutePath(String curDir) { /* * This algorithm involves scanning through a StringBuffer (buf) and * keep track of the various '/' characters found (these delineate the * various path components). There are several cases to handle: 1) * Component "." is found - simply delete it and keep scanning. 2) * Component "/" is found - this is an excessive /, and can also be * deleted. 3) Component ".." is found - we need to backtrack and delete * the previous component as well. * * We use two pointers (thisIndex, nextIndex) to track the start and end * of each component. We also use a Stack (slashStack) to record the * positions of all previous / characters, in case we need to backtrack. */ if (curDir == null) { return null; } StringBuffer buf = new StringBuffer(curDir); int maxIndex = buf.length(); int thisIndex = buf.indexOf("/"); Stack<Integer> slashStack = new Stack<Integer>(); /* * While not at the end of the string yet, keep looking for more * components. */ while ((thisIndex != -1) && (thisIndex != maxIndex)) { /* * Find the next / character. If there aren't any more, use the end * of string. This will allow us to find the next "component" * (between slashes). */ int nextIndex = buf.indexOf("/", thisIndex + 1); if (nextIndex == -1) { nextIndex = maxIndex; } String pathPart = buf.substring(thisIndex, nextIndex); /* a path component of "/." or "/" should be removed */ if (pathPart.equals("/.") || pathPart.equals("/")) { buf.delete(thisIndex, nextIndex); maxIndex -= (nextIndex - thisIndex); /* a path component of "/.." involves going backwards */ } else if (pathPart.equals("/..")) { /* * Find the index of the previous component's starting point by * popping the stack. If we go off the end of the stack, use * index 0 instead. This allows for paths like "/../.." that are * still legal in Linux. */ int lastIndex; try { lastIndex = slashStack.pop(); } catch (EmptyStackException ex) { lastIndex = 0; } buf.delete(lastIndex, nextIndex); maxIndex -= (nextIndex - lastIndex); thisIndex = lastIndex; } else { /* * normal case - record the slash position (in case we see a * future ..) and move on */ slashStack.push(Integer.valueOf(thisIndex)); thisIndex = nextIndex; } } /* * Return the normalized string. Note the special cases of "/.." that * should result in "/". */ if (buf.length() == 0) { return "/"; } else { return buf.toString(); } } /*-------------------------------------------------------------------------------------*/ /** * Determine whether a file system path is absolute or relative. * * @param path The path in question. * @return True if absolute, else false. */ public static boolean isAbsolutePath(String path) { return ((path != null) && (!path.isEmpty()) && ((path.charAt(0) == '/') || (path.charAt(0) == '\\'))); } /*-------------------------------------------------------------------------------------*/ /** * Given a path-like string, break it into separate file/directory * component names. For example, "/a/b/c" will be broken into {"a", "b", "c"}. * * @param path The full path * @return An array of String, one element for each component. */ public static String[] tokenizePath(String path) { List<String> resultList = new ArrayList<String>(); /* null path return an empty array */ if (path == null) { return new String[]{}; } /* * Traverse the path string, dividing it on / boundaries. */ int startIndex = 0; int endIndex; boolean done = false; do { /* * Find the next / in the string, until the end of string * is reached. */ int slashIndex = path.indexOf('/', startIndex); if (slashIndex == -1){ endIndex = path.length(); done = true; } else { endIndex = slashIndex; } /* if there's some content in the path component */ if (endIndex > startIndex) { String cmpt = path.substring(startIndex, endIndex); resultList.add(cmpt); } startIndex = slashIndex + 1; } while (!done); /* return the resulting list of path components */ return resultList.toArray(new String[0]); } /*-------------------------------------------------------------------------------------*/ /** * Given an array of file system paths, detect whether this specific path falls within * one of those directories (known as the "root"). If not, return null. If so, return * the portion of the path with the root removed. * <p> * For example, with pathRoots equal to { "/home/fred/", "/tmp" } and path equal to * "/home/fred/src/foo.c", return "src/foo.c". * * @param pathRoots An array of path roots, in the form of absolute paths. * @param path An absolute path that may or may not fall within one of the roots. * @return The portion of the path, with the matching root extracted, or null if the * path isn't within one of the roots. */ public static String matchPathRoot(String [] pathRoots, String path) { for (String root : pathRoots) { /* this path matches the root? */ if (path.startsWith(root)) { return path.substring(root.length()); } } /* no match */ return null; } /*-------------------------------------------------------------------------------------*/ }