/*
* sulky-resources - inheritance-safe class resources.
* Copyright (C) 2002-2011 Joern Huxhorn
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/*
* Copyright 2002-2011 Joern Huxhorn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package de.huxhorn.sulky.resources;
import java.util.ArrayList;
import java.util.List;
import java.util.StringTokenizer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* DOCUMENT: <code>PathTools</code>
* ..-like path elements
* In addition to the traditional "." and ".." path elements this class also supports
* a shorthand version for path structures like "../.." which can be written as "...".
*/
public final class PathTools
{
private PathTools()
{}
/**
* This method will resolve the given <code>path</code> in relation
* with <code>basePath</code> if necessary.
* If <code>path</code> is a relative path it is appended to <code>basePath</code>
* and returned. Otherwise it is returned unchanged.
* An empty string is considered a relative path.
* Possibly contained dots are NOT evaluated/reduced!
*
* Examples:
* resolvePath("foo","/bar") returns "/bar"
* resolvePath("/foo","bar") returns "/foo/bar"
* resolvePath("/foo","../bar") returns "/foo/../bar"
*
* @param basePath the base-path needed in case of an relative path.
* @param path the path that will be resolved against <code>basePath</code> if it
* is relative.
* @return the new path that was resolved according to <code>basePath</code>
* if necessary.
* @throws IllegalArgumentException if either basePath or path are null.
* @see #evaluatePath(java.lang.String) evaluatePath can be used to evaluate/reduce dots
* contained in resolved paths.
*/
public static String resolvePath(String basePath, String path)
{
final Logger logger = LoggerFactory.getLogger(PathTools.class);
if(basePath == null)
{
IllegalArgumentException ex = new IllegalArgumentException("basePath must not be null!");
if(logger.isDebugEnabled())
{
logger.debug("Parameter 'basePath' of method 'resolvePath' must not be null!", ex);
}
throw ex;
}
if(path == null)
{
IllegalArgumentException ex = new IllegalArgumentException("path must not be null!");
if(logger.isDebugEnabled()) logger.debug("Parameter 'path' of method 'resolvePath' must not be null!", ex);
throw ex;
}
if(path.startsWith("/"))
{
// path is absolute
return path;
}
if(basePath.length() == 0)
{
return path;
}
if(path.length() == 0)
{
return basePath;
}
if(!basePath.endsWith("/"))
{
basePath = basePath + "/";
}
return basePath + path;
}
/**
* This method returns the given path after evaluating contained
* ..-style path elements, e.g. "/foo/bar/../foobar" will be evaluated
* to "/foo/foobar". You can use more dots to get further up the hierarchy,
* e.g. "/foo/bar/..../foobar" will be evaluated to "../foobar".
*
* @param path the path to be evaluated
* @return a path that contains at most one single ..-style path element as the first element.
* @throws IllegalArgumentException if path is null.
* @see #resolvePath(java.lang.String, java.lang.String) resolvePath can be used to obtain a single path from a basePath and another path.
*/
public static String evaluatePath(String path)
{
List<String> pathStack = getPathStack(path, true);
return getPathStackString(pathStack);
}
/**
* Returns either the absolute path for the given basePath and path or null if
* the resulting path would not be absolute.
*
* Examples:
* getAbsolutePath("/foo/bar", "../foobar") returns "/foo/foobar"
* getAbsolutePath("/foo/bar", "..../foobar") returns null (path-underflow)
* getAbsolutePath("bar", "foobar") returns null (basePath not absolute)
*
* @param basePath the base path that is used to evaluate path.
* @param path the relative path.
* @return the absolute path or null if no such path was found, e.g. because
* basePath wasn't absolute or because a path-underflow happened.
* @throws IllegalArgumentException if either basePath or path is null.
*/
public static String getAbsolutePath(String basePath, String path)
{
String resolvedPath = resolvePath(basePath, path);
String result = evaluatePath(resolvedPath);
if(!result.startsWith("/"))
{
final Logger logger = LoggerFactory.getLogger(PathTools.class);
// path is not absolute - returning null...
if(logger.isDebugEnabled())
{
logger
.debug("The evaluated path is not absolute: \"" + result + "\". Returning null for getAbsolutePath(\"" + basePath + "\", \"" + path + "\").");
}
return null;
}
return result;
}
/**
* Returns the parent of the given path. ..-style path elements will be evaluated
* in the process.
* It's essentially a shortcut for evaluatePath(resolvePath(path, "..")).
*
* @param path the path for which the parent should be resolved.
* @return the parent of the given path
*/
public static String getParentPath(String path)
{
return evaluatePath(resolvePath(path, ".."));
}
public static String getCompatiblePath(String path)
{
List<String> pathStack = getPathStack(path, true);
if(pathStack.isEmpty())
{
return "";
}
String first = pathStack.get(0);
if(isDotPattern(first))
{
pathStack.remove(0);
int len = first.length();
for(int i = 1; i < len; i++)
{
pathStack.add(0, "..");
}
}
return getPathStackString(pathStack);
}
/**
* Returns <code>true</code> if the given String contains only dots.
*
* @param s the string that is checked for dots.
* @return <code>true</code> if the given String contains only dots
*/
public static boolean isDotPattern(String s)
{
for(int i = 0; i < s.length(); i++)
{
char c = s.charAt(i);
if(c != '.')
{
return false;
}
}
return true;
}
/**
* Returns a stack containing all the path elements of the given path.
* if evaluateDots is true the resulting stack will have at most one ..-like
* path element as the first element.
* If the resulting path is an absolute path the first element of the stack will be "/".
*
* @param path the input path
* @param evaluateDots if <code>true</code>, dots will be evaluated.
* @return a Stack containing all the path elements of path.
*/
public static List<String> getPathStack(String path, boolean evaluateDots)
{
List<String> pathStack = new ArrayList<>();
boolean wasAbsolute = false;
if(path.startsWith("/"))
{
// remember absolute path...
wasAbsolute = true;
}
int underflowCounter = 0;
StringTokenizer tok = new StringTokenizer(path, "/", false);
while(tok.hasMoreTokens())
{
String pathElement = tok.nextToken();
if(pathElement.length() != 0)
{
// ignore wrong //-entries
if(evaluateDots)
{
if(isDotPattern(pathElement))
{
// we found a dot-element
for(int i = 1; i < pathElement.length(); i++)
{
if(pathStack.isEmpty())
{
underflowCounter++;
}
else
{
pathStack.remove(pathStack.size()-1);
}
}
}
else
{
pathStack.add(pathElement);
}
}
else
{
pathStack.add(pathElement);
}
}
}
if(underflowCounter > 0)
{
StringBuilder dots = new StringBuilder(".");
for(int i = 0; i < underflowCounter; i++)
{
dots.append(".");
}
pathStack.add(0, dots.toString());
}
else if(wasAbsolute)
{
pathStack.add(0, "/");
}
return pathStack;
}
/**
* The pathStack itself is emptied in the process.
*
* @param pathStack the path stack.
* @return the string representation of the given path stack.
*/
public static String getPathStackString(List<String> pathStack)
{
StringBuilder result = new StringBuilder();
boolean rootFound = false;
if(!pathStack.isEmpty())
{
rootFound = true;
String currentEntry = pathStack.remove(0);
if(!"/".equals(currentEntry))
{
rootFound = false;
result.append(currentEntry);
}
while(!pathStack.isEmpty())
{
result.append("/");
result.append(pathStack.remove(0));
}
}
if(rootFound && result.length() == 0)
{
result.append("/");
}
return result.toString();
}
}