/* (c) 2014 - 2016 Open Source Geospatial Foundation - all rights reserved
* (c) 2014 OpenPlans
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.platform.resource;
import java.io.File;
import java.net.URI;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Pattern;
/**
* Utility class for handling Resource paths in a consistent fashion.
* <p>
* This utility class is primarily aimed at implementations of ResourceStore and may be helpful when writing test cases. These methods are suitable
* for static import.
* <p>
* Resource paths are consistent with file URLs. The base location is represented with "", relative paths are not supported.
*
* @author Jody Garnett
*/
public class Paths {
/**
* Path to base resource.
*/
public static final String BASE = "";
static String parent(String path) {
if (path == null) {
return null;
}
int last = path.lastIndexOf('/');
if (last == -1) {
if (BASE.equals(path)) {
return null;
} else {
return BASE;
}
} else {
return path.substring(0, last);
}
}
static String name(String path) {
if (path == null) {
return null;
}
int last = path.lastIndexOf('/');
if (last == -1) {
return path; // top level resource
} else {
String item = path.substring(last + 1);
return item;
}
}
/** Used to quickly check path extension */
static String extension(String path) {
String name = name(path);
if (name == null) {
return null;
}
int last = name.lastIndexOf('.');
if (last == -1) {
return null; // no extension
} else {
return name.substring(last + 1);
}
}
static String sidecar(String path, String extension) {
if (extension == null) {
return null;
}
int last = path.lastIndexOf('.');
if (last == -1) {
return path + "." + extension;
} else {
return path.substring(0, last) + "." + extension;
}
}
/**
* Path construction.
*
* @param path Items defining a Path
* @return path Path used to identify a Resource
*/
public static String path(String... path) {
return path(STRICT_PATH, path);
}
/**
* Path construction.
*
* @param strictPath whether problematic characters are an error
* @param path Items defining a Path
* @return path Path used to identify a Resource
*/
static String path(boolean strictPath, String... path) {
if (path == null || (path.length == 1 && path[0] == null)) {
return null;
}
ArrayList<String> names = new ArrayList<String>();
for (String item : path) {
names.addAll(names(item));
}
return toPath(strictPath, names);
}
// runtime flag which, if true, throws an error for the WARN characters
static final boolean STRICT_PATH =
Boolean.valueOf(System.getProperty("STRICT_PATH", "false"));
/**
* Pattern used to check for invalid file characters.
* <ul>
* <li> backslash
* </ul>
*/
static final Pattern VALID = Pattern.compile("^[^\\\\]*$");
/**
* Pattern used to check for ill-advised file characters:
* <ul>
* <li> star
* <li> colon - potential conflict with xml prefix separator and workspace style separator
* <li> comma
* <li> single quote
* <li> ampersand
* <li> question mark
* <li> double quote
* <li> less than
* <li> greater than
* <li> bar
* </ul>
* These characters can cause problems for different protocols.
*/
static final Pattern WARN = Pattern.compile("^[^:*,\'&?\"<>|]*$");
/**
* Set of invalid resource names (currently used to quickly identify relative paths).
*/
static final Set<String> INVALID = new HashSet<String>(
Arrays.asList(new String[] { "..", "." }));
/**
* Internal method used to convert a list of names to a normal Resource path.
*
* @param strictPath whether problematic characters are an error
* @param names List of resource names forming a path
* @return resource path composed of provided names
* @throws IllegalArgumentException If names includes any {@link #INVALID} chracters
*/
private static String toPath(boolean strictPath, List<String> names) {
StringBuilder buf = new StringBuilder();
final int LIMIT = names.size();
for (int i = 0; i < LIMIT; i++) {
String item = names.get(i);
if (item == null) {
continue; // skip null names
}
if (INVALID.contains(item)) {
throw new IllegalArgumentException("Contains invalid " + item + " path: " + buf);
}
if (!VALID.matcher(item).matches()) {
throw new IllegalArgumentException("Contains invalid " + item + " path: " + buf);
}
if (!WARN.matcher(item).matches()) {
if (strictPath) {
throw new IllegalArgumentException("Contains invalid " + item + " path: " + buf);
}
}
buf.append(item);
if (i < LIMIT - 1) {
buf.append("/");
}
}
return buf.toString();
}
/**
* Quick check of path for invalid characters
*
* @param path
* @return path
* @throws IllegalArgumentException If path fails {@link #VALID} check
*/
public static String valid(String path) {
return path(STRICT_PATH, path);
}
/**
* Quick check of path for invalid characters
*
* @param strictPath whether problematic characters are an error
* @param path
* @return path
* @throws IllegalArgumentException If path fails {@link #VALID} check
*/
static String valid(boolean strictPath, String path) {
if (path == null) {
throw new NullPointerException("Resource path required");
}
if( path.contains("..") || ".".equals(path)) {
throw new IllegalArgumentException("Relative paths not supported " + path);
}
if (!VALID.matcher(path).matches()) {
throw new IllegalArgumentException("Contains invalid chracters " + path);
}
if (!WARN.matcher(path).matches()) {
if (strictPath) {
throw new IllegalArgumentException("Contains invalid chracters " + path);
}
}
return path;
}
public static List<String> names(String path) {
if (path == null || path.length() == 0) {
return Collections.emptyList();
}
int index = 0;
int split = path.indexOf('/');
if (split == -1) {
return Collections.singletonList(path);
}
ArrayList<String> names = new ArrayList<String>(3);
String item;
do {
item = path.substring(index, split);
if (item.length() != 0 && item != "/") {
names.add(item);
}
index = split + 1;
split = path.indexOf('/', index);
} while (split != -1);
item = path.substring(index);
if (item != null && item.length() != 0 && item != "/") {
names.add(item);
}
return names;
}
/**
* Convert to file to resource path.
*
* @param base directory location
* @param file relative file reference
* @return relative path used for Resource lookup
*/
public static String convert(File base, File file) {
if (base == null) {
if (file.isAbsolute()) {
throw new IllegalArgumentException("Unable to determine relative path as file was absolute");
} else {
return convert(file.getPath());
}
}
if (file == null) {
return Paths.BASE;
}
URI baseURI = base.toURI();
URI fileURI = file.toURI();
if (fileURI.toString().startsWith(baseURI.toString())) {
URI relativize = baseURI.relativize(fileURI);
return relativize.getPath();
} else {
return convert(file.getPath());
}
}
/**
* Convert to file to resource path, allows for relative references (but is limited to content within the provided base directory).
*
*
* @param base directory location
* @param folder context for relative path (may be "." or null for base directory)
* @param fileLocation File path (using {@link File#separator}) allowing for relative references
* @return relative path used for Resource lookup
*/
public static String convert(File base, File folder, String fileLocation) {
if (base == null) {
throw new NullPointerException("Base directory required for relative path");
}
List<String> folderPath = names(convert(base, folder));
List<String> filePath = names(convert(fileLocation));
List<String> resolvedPath = new ArrayList<String>(folderPath.size() + filePath.size());
resolvedPath.addAll(folderPath);
for (String item : filePath) {
if (item == null)
continue;
if (item.equals("."))
continue;
if (item.equals("..")) {
if (!resolvedPath.isEmpty()) {
resolvedPath.remove(resolvedPath.size() - 1);
continue;
} else {
throw new IllegalStateException("File location " + fileLocation
+ " outside of " + base.getPath());
}
}
resolvedPath.add(item);
}
return toPath(STRICT_PATH, resolvedPath);
}
/**
* Convert to file to resource path, allows for relative references (but is limited to content within the provided base directory).
*
*
* @param base directory location
* @param folder context for relative path (may be "." or null for base directory)
* @param location File path (using {@link File#separator}) allowing for relative references
* @return relative path used for Resource lookup
*/
public static String convert(File base, File folder, String... location) {
if (base == null) {
throw new NullPointerException("Base directory required for relative path");
}
List<String> folderPath = names(convert(base, folder));
List<String> filePath = Arrays.asList(location);
List<String> resolvedPath = new ArrayList<String>(folderPath.size() + filePath.size());
resolvedPath.addAll(folderPath);
for (String item : filePath) {
if (item == null)
continue;
if (item.equals("."))
continue;
if (item.equals("..")) {
if (!resolvedPath.isEmpty()) {
resolvedPath.remove(resolvedPath.size() - 1);
continue;
} else {
throw new IllegalStateException("File location " + filePath + " outside of "
+ base.getPath());
}
}
resolvedPath.add(item);
}
return toPath(STRICT_PATH, resolvedPath);
}
/**
* Convert a filePath to resource path (supports absolute paths).
*
* This method converts file paths (using {@link File#separator}) to the URL style paths used for {@link ResourceStore#get(String)}.
*
* @param filePath File path using {@link File#separator}
* @return Resource path suitable for use with {@link ResourceStore#get(String)} or null for absolute path
*/
public static String convert(String filePath) {
if (filePath == null) {
return null;
}
if (filePath.length() == 0) {
return filePath;
}
if (File.separatorChar == '/') {
return filePath;
} else {
return filePath.replace(File.separatorChar, '/');
}
}
/**
* Convert a filePath to resource path (starting from the provided path). Absolute file paths are not supported, and the final resource must still
* be within the data directory.
*
* This method converts file paths (using {@link File#separator}) to the URL style paths used for {@link ResourceStore#get(String)}.
*
* @param path Initial path used resolve relative reference lookup
* @param filename File path (using {@link File#separator})
* @return Resource path suitable for use with {@link ResourceStore#get(String)} or null for absolute path
*/
public static String convert(String path, String filename) {
if (path == null) {
throw new NullPointerException("Initial path required to handle relative filenames");
}
List<String> folderPath = names(path);
List<String> filePath = names(convert(filename));
List<String> resolvedPath = new ArrayList<String>(folderPath.size() + filePath.size());
resolvedPath.addAll(folderPath);
for (String item : filePath) {
if (item == null)
continue;
if (item.equals("."))
continue;
if (item.equals("..")) {
if (!resolvedPath.isEmpty()) {
resolvedPath.remove(resolvedPath.size() - 1);
continue;
} else {
throw new IllegalStateException("File location " + filename + " outside of "
+ path);
}
}
resolvedPath.add(item);
}
return toPath(STRICT_PATH, resolvedPath);
}
/**
* Convert a Resource path to file reference for provided base directory.
* <p>
* This method requires the base directory of the ResourceStore.
* Note ResourceStore implementations may not create the file until needed.
* In the case of an absolute path, base should be null.
*
* @param base Base directory, often GeoServer Data Directory
* @param path Resource path reference
* @return File reference
*/
public static File toFile( File base, String path ){
for( String item : Paths.names(path) ){
base = new File( base, item );
}
return base;
}
}