/*
* Copyright 2003-2016 JetBrains s.r.o.
*
* 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 jetbrains.mps.vfs.path;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.mps.annotations.Immutable;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.StringJoiner;
import java.util.StringTokenizer;
import java.util.stream.Collectors;
/**
* Represents a familiar path to any file or folder on disk.
* Also might be a path within the archive (from the archive root)
* As any path a {@code CommonPath} represents a wrapper around path (= string).
*
* To work with archives consider using {@link UniPath}.
*
* TODO create smth like FileSystem with WinFileSystem and UnixFileSystem, refactor this class
*
* Created by apyshkin on 6/17/16.
*/
@Immutable
public final class CommonPath extends AbstractPath {
private static final Logger LOG = LogManager.getLogger(CommonPath.class);
private final static String PARENT_DIR_STR = "..";
private final static String CUR_DIR_STR = ".";
private final static String DEFAULT_UNIX_ROOT = "";
private final static String WIN_ROOT_SEP = ":\\";
private final static String UNIX_ROOT_SEP = UNIX_SEPARATOR;
private final String myPath; // equals to {@link #joinParts} contains root
private final String myRoot; // can be drive letter on windows (X) or DEFAULT_UNIX_ROOT on UNIX only!
private final List<String> myParts; // the min length is zero
private final boolean myRelativeFlag; // true <=> myRoot != null
private final String mySeparator;
private final char mySeparatorChar;
private final String myRootSeparator;
private CommonPath(@NotNull String path) {
boolean isWin = path.contains(WIN_SEPARATOR);
mySeparatorChar = isWin ? WIN_SEPARATOR_CHAR : UNIX_SEPARATOR_CHAR;
mySeparator = String.valueOf(mySeparatorChar);
myRoot = parseRoot(path);
myRelativeFlag = (myRoot == null);
myRootSeparator = calcRootSeparator();
path = trimRootAndSeparators(path);
myParts = parseRelativeParts(path);
myPath = joinParts();
}
@Nullable
private String parseRoot(@NotNull String path) {
boolean relativeFlag;
String root;
if (isWindows()) {
relativeFlag = path.isEmpty() || !path.contains(WIN_ROOT_SEP);
root = relativeFlag ? null : path.substring(0, path.indexOf(WIN_ROOT_SEP));
} else {
relativeFlag = path.isEmpty() || path.charAt(0) != UNIX_SEPARATOR_CHAR;
root = relativeFlag ? null : DEFAULT_UNIX_ROOT;
}
return root;
}
@NotNull
private List<String> parseRelativeParts(@NotNull String path) {
List<String> result = new ArrayList<>();
StringTokenizer tokenizer = new StringTokenizer(path, mySeparator);
while (tokenizer.hasMoreTokens()) {
String token = tokenizer.nextToken();
if (!token.isEmpty()) {
result.add(token);
}
}
return result;
}
/**
* @param parts each of them must not be null.
*/
private CommonPath(char separator, @Nullable String root, String... parts) {
if (parts == null || parts.length == 0 || parts[0] == null) {
parts = new String[0];
}
mySeparatorChar = separator;
mySeparator = String.valueOf(mySeparatorChar);
myRelativeFlag = root == null;
myRootSeparator = calcRootSeparator();
if (!myRelativeFlag && !isWindows() && !root.equals(DEFAULT_UNIX_ROOT)) {
throw new InvalidPathException(root, "In UNIX root must be always equal to `" + DEFAULT_UNIX_ROOT + "'.");
}
myRoot = root;
myParts = Arrays.asList(parts).stream().map(path -> path + "").collect(Collectors.toList());
myPath = joinParts();
}
private String calcRootSeparator() {
if (myRelativeFlag) {
return null;
}
return isWindows() ? WIN_ROOT_SEP : UNIX_ROOT_SEP;
}
private String joinParts() {
StringJoiner joiner = new StringJoiner(mySeparator);
myParts.forEach(joiner::add);
String partsString = joiner.toString();
if (isRelative()) {
return partsString;
}
return myRoot + myRootSeparator + partsString;
}
private CommonPath(char separator, @Nullable String root, @NotNull List<String> parts) {
this(separator, root, parts.toArray(new String[parts.size()]));
}
public final boolean isWindows() {
return mySeparatorChar != UNIX_SEPARATOR_CHAR;
}
char getSeparatorChar() {
return mySeparatorChar;
}
/**
* remove all doubling separators specifically in the start and the end of the path
*/
private String trimRootAndSeparators(@NotNull String path) {
if (!isRelative()) {
path = path.substring(myRoot.length() + myRootSeparator.length());
}
int lastNonSepSymb = path.length() - 1;
while (lastNonSepSymb >= 0 && path.charAt(lastNonSepSymb) == mySeparatorChar) {
--lastNonSepSymb;
}
int index = 0;
char[] result = new char[path.length()];
for (int i = 0; i <= lastNonSepSymb; ++i) {
if (index > 0 && result[index - 1] == mySeparatorChar && mySeparatorChar == path.charAt(i)) {
continue;
}
result[index++] = path.charAt(i);
}
return String.valueOf(result, 0, index);
}
private static void validate(@NotNull String path) {
if (path.contains(Path.UNIX_SEPARATOR) && path.contains(Path.WIN_SEPARATOR)) {
LOG.warn("Path " + path + " contains both unix and windows separators.");
}
if (path.contains(Path.ARCHIVE_SEPARATOR)) {
throw new InvalidPathException(path, "CommonPath is not allowed to include archive separators. One would expect UniPath to be used here.");
}
}
/**
* parses the path string and creates a common path from it
*/
public static CommonPath fromString(@NotNull String path) {
validate(path);
return new CommonPath(path);
}
/**
* creates an instance
* @param separator default separator for the path
* @param root might be drive letter on windows or {@link #DEFAULT_UNIX_ROOT} on UNIX. NB: in the case of null the path will be relative!
* @param parts each of the parts must not be null
*/
public static CommonPath fromParts(char separator, @Nullable String root, @Nullable String... parts) {
root = validateRoot(root, separator);
validateParts(parts);
return new CommonPath(separator, root, parts);
}
private static String validateRoot(String root, char separator) {
if (separator == WIN_SEPARATOR_CHAR) {
if (root != null && root.contains(String.valueOf(separator))) {
throw new InvalidPathException(root, "The root is not allowed to contain separator " + separator);
}
} else if (separator == UNIX_SEPARATOR_CHAR) {
if (root != null && (!root.equals(DEFAULT_UNIX_ROOT) && !root.equals(UNIX_ROOT_SEP))) {
throw new InvalidPathException(root, "The UNIX root is allowed to be `" + DEFAULT_UNIX_ROOT + "' or `" + UNIX_ROOT_SEP + "'.");
}
if (root != null && root.equals(UNIX_ROOT_SEP)) { // hack to improve user experience
root = DEFAULT_UNIX_ROOT;
}
}
return root;
}
private static void validateParts(String[] parts) {
if (parts != null) {
for (String part : parts) {
if (part == null) {
throw new InvalidPathException(Arrays.toString(parts), "The null parts are not allowed");
}
}
}
}
@Override
public boolean isRelative() {
assert myRelativeFlag == (myRoot == null);
return myRelativeFlag;
}
@Override
public char getSeparator() {
return mySeparatorChar;
}
@NotNull
@Override
public List<String> getNames() {
if (myRelativeFlag) {
return Collections.unmodifiableList(myParts);
} else {
List<String> res = new ArrayList<>();
assert myRoot != null;
res.add(myRoot);
res.addAll(myParts);
return Collections.unmodifiableList(res);
}
}
@Nullable
@Override
public Path getRoot() {
return CommonPath.fromParts(mySeparatorChar, myRoot);
}
@Override
@Nullable
public CommonPath getParent() {
if (myParts.isEmpty()) {
return null;
}
List<String> parentParts = myParts.subList(0, myParts.size() - 1);
return new CommonPath(mySeparatorChar, myRoot, parentParts);
}
@Override
@NotNull
public CommonPath toIndependentPath() {
if (mySeparatorChar == UNIX_SEPARATOR_CHAR) {
return copy();
}
if (!isRelative()) {
throw new InvalidPathException(myPath, "Cannot convert absolute windows path to unix path");
}
assert myRoot == null;
return new CommonPath(UNIX_SEPARATOR_CHAR, null, myParts);
}
@Override
@NotNull
public CommonPath toSystemPath() {
if (mySeparatorChar == SYSTEM_SEPARATOR_CHAR) {
return copy();
}
if (!isRelative()) {
throw new InvalidPathException(myPath, "Cannot convert absolute windows path to unix path and vice versa");
}
assert myRoot == null;
return new CommonPath(SYSTEM_SEPARATOR_CHAR, null, myParts);
}
@Override
public boolean endsWith(@NotNull String other) {
return endsWith(CommonPath.fromString(other));
}
@Override
public boolean startsWith(@NotNull String other) {
return startsWith(CommonPath.fromString(other));
}
@NotNull
@Override
public CommonPath relativize(@NotNull Path other) {
return null;
}
@NotNull
@Override
public CommonPath toAbsolute() {
return CommonPath.fromString(new File(myPath).getAbsolutePath());
}
@NotNull
@Override
public final CommonPath toNormal() {
List<String> newParts = new ArrayList<>();
for (String part : myParts) {
if (part.equals(PARENT_DIR_STR)) {
if (!newParts.isEmpty() && !newParts.get(newParts.size() - 1).equals(PARENT_DIR_STR)) {
newParts.remove(newParts.size() - 1);
continue;
}
} else if (part.equals(CUR_DIR_STR)) {
continue;
}
newParts.add(part);
}
return new CommonPath(mySeparatorChar, myRoot, newParts);
}
@NotNull
@Override
public CommonPath toCanonical() throws IOException {
return new CommonPath(new File(myPath).getCanonicalPath());
}
@NotNull
@Override
public Path resolve(@NotNull Path other) {
return null;
}
@NotNull
@Override
public CommonPath resolve(@NotNull String other) {
return null;
}
@Override
public int compareTo(@NotNull Path path) {
return toString().compareTo(path.toString()); //FIXME
}
@Override
public String toString() {
return myPath;
}
@NotNull
@Override
public CommonPath copy() {
return new CommonPath(mySeparatorChar, myRoot, myParts);
}
}