com.palantir.giraffe.file.base.ImmutableListPathCore.java Source code

Java tutorial

Introduction

Here is the source code for com.palantir.giraffe.file.base.ImmutableListPathCore.java

Source

/**
 * Copyright 2015 Palantir Technologies, Inc.
 *
 * 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 com.palantir.giraffe.file.base;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkElementIndex;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkPositionIndex;

import java.nio.file.InvalidPathException;
import java.util.Collections;
import java.util.Objects;
import java.util.regex.Pattern;

import javax.annotation.Nullable;

import com.google.common.base.Joiner;
import com.google.common.collect.ComparisonChain;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Ordering;
import com.palantir.giraffe.file.base.ImmutableListPathCore.Parser.NamedRootFormat;

/**
 * A {@link PathCore} implementation backed by an {@link ImmutableList} of
 * segments.
 *
 * @author bkeyes
 */
public final class ImmutableListPathCore implements PathCore<ImmutableListPathCore> {

    private static final String CURRENT_DIR = ".";
    private static final String PARENT_DIR = "..";

    private static final ImmutableListPathCore emptyInstance = new ImmutableListPathCore(null,
            ImmutableList.<String>of());

    /**
     * Constructs a new parser for path strings with a single root. Each segment
     * of the path is separated by the given string. Absolute paths start with
     * the given string.
     *
     * @param separator the path separator
     *
     * @return a new parser
     */
    public static Parser parser(String separator) {
        return new Parser(separator, null, null);
    }

    /**
     * Constructs a new parser for path strings with named roots. Each segment
     * of the path is separated by the given separator string.
     * <p>
     * Roots are parsed based on the root format and the root identifier string.
     * For {@linkplain NamedRootFormat#PREFIX prefix} roots, absolute paths
     * start with the identifier string. The root name consists of all
     * characters from the start of the string to the first path separator,
     * exclusive. For {@linkplain NamedRootFormat#POSTFIX postfix} roots,
     * absolute paths contain the identifier string. The root name consists of
     * all characters from the start of the string to the identifier, inclusive.
     * The path separator must immediately follow the identifier.
     * <p>
     * For {@linkplain NamedRootFormat#OPTIONAL_PREFIX optional prefix} roots,
     * absolute paths start with either the identifier string as described above
     * or the path separator. Absolute paths that start with the path separator
     * have an unnamed (empty) root.
     *
     * @param separator the path separator
     * @param format the root format
     * @param rootId the root identifier
     *
     * @return a new parser
     */
    public static Parser namedRootParser(String separator, NamedRootFormat format, String rootId) {
        checkNotNull(format, "format must be non-null");
        checkNotNull(rootId, "rootId must be non-null");
        checkArgument(!rootId.isEmpty(), "rootId must be non-empty for named roots");
        return new Parser(separator, format, rootId);
    }

    /**
     * Creates {@code ImmutableListPathCore} instances from path strings.
     *
     * @author bkeyes
     */
    public static final class Parser {

        /**
         * Specifies the format of named roots in a path string.
         */
        public enum NamedRootFormat {
            /**
             * All roots are identified by a prefix string.
             */
            PREFIX,

            /**
             * Named roots are identified by a prefix string while the unamed
             * root is identified by the path separator.
             */
            OPTIONAL_PREFIX,

            /**
             * All roots are identified by a postfix string.
             */
            POSTFIX;
        }

        private final String separator;
        private final NamedRootFormat rootFormat;
        private final String rootId;

        private Parser(String separator, NamedRootFormat rootFormat, String rootId) {
            this.separator = checkNotNull(separator, "separator must be non-null");
            checkArgument(!separator.isEmpty(), "separator must be non-empty");

            this.rootFormat = rootFormat;
            this.rootId = rootId;
        }

        public ImmutableListPathCore parse(String path) {
            checkCharacters(path);

            int pathStart = 0;
            String root = null;

            if (isRootPrefix(rootFormat) && path.startsWith(rootId)) {
                int end = path.indexOf(separator, rootId.length());
                if (end == rootId.length()) {
                    throw new InvalidPathException(path, "root name is empty", end);
                } else if (end < 0) {
                    throw new InvalidPathException(path, "root is not terminated", path.length());
                }
                root = path.substring(0, end);
                pathStart = end + separator.length();
            } else if (rootFormat == NamedRootFormat.POSTFIX) {
                int id = path.indexOf(rootId);
                if (id > 0) {
                    int rootEnd = id + rootId.length();
                    root = path.substring(0, rootEnd);
                    checkPostfixSeparator(path, rootEnd);
                    pathStart = rootEnd + separator.length();
                } else if (id == 0) {
                    throw new InvalidPathException(path, "root name is empty", id);
                }
            } else if (isRootSeparator(rootFormat) && path.startsWith(separator)) {
                root = "";
                pathStart = separator.length();
            }

            ImmutableList.Builder<String> validSegments = ImmutableList.builder();
            for (String segment : path.substring(pathStart).split(Pattern.quote(separator))) {
                if (!segment.isEmpty()) {
                    validSegments.add(segment);
                }
            }
            return new ImmutableListPathCore(root, validSegments.build());
        }

        private static void checkCharacters(String path) {
            int nullIndex = path.indexOf('\u0000');
            if (nullIndex >= 0) {
                throw new InvalidPathException(path, "NUL character not allowed", nullIndex);
            }
        }

        private static boolean isRootPrefix(NamedRootFormat type) {
            return type == NamedRootFormat.PREFIX || type == NamedRootFormat.OPTIONAL_PREFIX;
        }

        private static boolean isRootSeparator(NamedRootFormat type) {
            return type == null || type == NamedRootFormat.OPTIONAL_PREFIX;
        }

        private void checkPostfixSeparator(String path, int end) {
            if (!separator.equals(path.substring(end, end + separator.length()))) {
                throw new InvalidPathException(path, "root is not follwed by separator", end);
            }
        }
    }

    @Nullable
    private final String root;
    private final ImmutableList<String> path;

    /**
     * Constructs a new core instance.
     *
     * @param root the root component, or {@code null} if this path is relative.
     * @param path the segments that make up this path
     */
    public ImmutableListPathCore(@Nullable String root, Iterable<String> path) {
        this.root = root;
        this.path = ImmutableList.copyOf(checkNotNull(path, "path must be non-null"));
    }

    @Override
    public boolean isAbsolute() {
        return root != null;
    }

    @Override
    public ImmutableListPathCore getRoot() {
        if (isAbsolute()) {
            return new ImmutableListPathCore(root, ImmutableList.<String>of());
        } else {
            return null;
        }
    }

    @Override
    public ImmutableListPathCore getFileName() {
        if (getNameCount() == 0) {
            return null;
        } else {
            return getName(getNameCount() - 1);
        }
    }

    @Override
    public ImmutableListPathCore getParent() {
        if (getNameCount() > (isAbsolute() ? 0 : 1)) {
            return new ImmutableListPathCore(root, path.subList(0, getNameCount() - 1));
        } else {
            return null;
        }
    }

    @Override
    public int getNameCount() {
        return path.size();
    }

    @Override
    public ImmutableListPathCore getName(int index) {
        checkElementIndex(index, getNameCount(), "index");
        return new ImmutableListPathCore(null, ImmutableList.of(path.get(index)));
    }

    @Override
    public ImmutableListPathCore subpath(int beginIndex, int endIndex) {
        checkPositionIndex(endIndex, getNameCount(), "endIndex");
        checkElementIndex(beginIndex, endIndex, "beginIndex");
        return new ImmutableListPathCore(null, path.subList(beginIndex, endIndex));
    }

    @Override
    public boolean startsWith(ImmutableListPathCore other) {
        if (other.isAbsolute() != isAbsolute()) {
            return false;
        } else if (other.path.size() > path.size()) {
            return false;
        } else {
            boolean rootStartsWith = Objects.equals(root, other.root);
            return rootStartsWith && other.path.equals(path.subList(0, other.path.size()));
        }
    }

    @Override
    public boolean endsWith(ImmutableListPathCore other) {
        if (other.isAbsolute()) {
            return isAbsolute() && this.equals(other);
        } else if (other.path.size() > path.size()) {
            return false;
        } else {
            int start = path.size() - other.path.size();
            return other.path.equals(path.subList(start, path.size()));
        }
    }

    @Override
    public ImmutableListPathCore normalize() {
        boolean[] ignore = new boolean[path.size()];
        int ignoreCount = 0;

        // mark redundant elements as ignored
        for (int i = 0; i < path.size(); i++) {
            if (path.get(i).equals(CURRENT_DIR)) {
                ignore[i] = true;
                ignoreCount++;
            } else if (path.get(i).equals(PARENT_DIR)) {
                // search backwards to find first unignored element
                int j = i - 1;
                while (j >= 0 && ignore[j]) {
                    j--;
                }

                if (j >= 0 && !path.get(j).equals(PARENT_DIR)) {
                    // path matches "name/[ignored]/.."
                    // ignore both name and ".."
                    ignore[i] = true;
                    ignore[j] = true;
                    ignoreCount += 2;
                } else if (j < 0 && isAbsolute()) {
                    // path matches "/[ignored]/.."
                    // ignore ".." (parent of / is /)
                    ignore[i] = true;
                    ignoreCount++;
                }
            }
        }

        if (ignoreCount == 0) {
            return this;
        } else {
            ImmutableList.Builder<String> normalized = ImmutableList.builder();
            for (int i = 0; i < path.size(); i++) {
                if (!ignore[i]) {
                    normalized.add(path.get(i));
                }
            }
            return new ImmutableListPathCore(root, normalized.build());
        }
    }

    @Override
    public ImmutableListPathCore resolve(ImmutableListPathCore other) {
        if (other.isAbsolute()) {
            return other;
        } else if (other.getNameCount() == 0) {
            return this;
        } else {
            ImmutableList<String> combinedNames = ImmutableList.<String>builder().addAll(path).addAll(other.path)
                    .build();
            return new ImmutableListPathCore(root, combinedNames);
        }
    }

    @Override
    public ImmutableListPathCore resolveSibling(ImmutableListPathCore other) {
        ImmutableListPathCore parent = getParent();
        return (parent == null) ? other : parent.resolve(other);
    }

    @Override
    public ImmutableListPathCore relativize(ImmutableListPathCore other) {
        if (this.equals(other)) {
            return emptyInstance;
        } else if (isAbsolute() && !other.isAbsolute()) {
            throw new IllegalArgumentException("cannot relativize from absolute path to relative path");
        } else if (!isAbsolute() && other.isAbsolute()) {
            throw new IllegalArgumentException("cannot relativize from relative path to absolute path");
        } else if (isAbsolute() && !getRootString().equals(other.getRootString())) {
            throw new IllegalArgumentException("cannot relativize between paths with different roots");
        } else {
            return relativizeNormalizedPaths(normalize(), other.normalize());
        }
    }

    private static ImmutableListPathCore relativizeNormalizedPaths(ImmutableListPathCore base,
            ImmutableListPathCore other) {
        if (base.equals(other)) {
            return emptyInstance;
        } else {
            // find longest common prefix
            int i = 0;
            int limit = Math.min(base.path.size(), other.path.size());
            while (i < limit && base.path.get(i).equals(other.path.get(i))) {
                i++;
            }

            ImmutableList.Builder<String> relative = ImmutableList.builder();
            // go up once for each remaining name in this path
            relative.addAll(Collections.nCopies(Math.max(0, base.path.size() - i), PARENT_DIR));
            // append the remaining names from the other path
            relative.addAll(other.path.subList(i, other.path.size()));
            return new ImmutableListPathCore(null, relative.build());
        }
    }

    @Override
    public String toPathString(String separator) {
        checkNotNull(separator, "separator must be non-null");
        checkArgument(!separator.isEmpty(), "separator must be non-empty");

        StringBuilder pathString = new StringBuilder();
        if (isAbsolute()) {
            pathString.append(root).append(separator);
        }
        Joiner.on(separator).appendTo(pathString, path);
        return pathString.toString();
    }

    @Override
    public String toString() {
        return "ImmutableListPathCore[root=" + root + ", path=" + path + "]";
    }

    @Override
    public int compareTo(ImmutableListPathCore other) {
        return ComparisonChain.start().compare(root, other.root, Ordering.natural().nullsLast())
                .compare(path, other.path, Ordering.<String>natural().lexicographical()).result();
    }

    @Override
    public boolean equals(Object obj) {
        if (obj == this) {
            return true;
        } else if (!(obj instanceof ImmutableListPathCore)) {
            return false;
        } else {
            ImmutableListPathCore other = (ImmutableListPathCore) obj;
            return Objects.equals(root, other.root) && path.equals(other.path);
        }
    }

    @Override
    public int hashCode() {
        return Objects.hash(root, path);
    }

    /**
     * Returns the root component of this path, or {@code null} if this path is
     * relative.
     */
    @Nullable
    public String getRootString() {
        return root;
    }

    public ImmutableList<String> getPathSegments() {
        return path;
    }
}