PathResolver.java

package network.ike.plugin.scaffold;

import java.nio.file.Path;
import java.util.Objects;

/**
 * Expands manifest {@code dest} strings to absolute filesystem paths.
 *
 * <p>Conventions:
 * <ul>
 *   <li>{@link ScaffoldScope#USER} entries start with {@code "~/"} and
 *       resolve against the caller-supplied user home.</li>
 *   <li>{@link ScaffoldScope#PROJECT} entries may optionally start
 *       with {@code "{project.root}/"} (the literal token, not a
 *       property reference); both forms resolve against the
 *       project-root path passed in.</li>
 * </ul>
 */
public final class PathResolver {

    private final Path userHome;
    private final Path projectRoot;

    /**
     * Construct a resolver bound to the given user-home and
     * project-root anchors. Either anchor may be used at resolve time
     * depending on the scope of the manifest entry; {@code projectRoot}
     * may be omitted when only USER-scope resolution is required.
     *
     * @param userHome    absolute path to the user's home directory;
     *                    required for USER-scope entries
     * @param projectRoot absolute path to the current project's root;
     *                    required for PROJECT-scope entries. May be
     *                    {@code null} when only USER-scope resolution
     *                    is expected (e.g. fresh-machine bootstrap).
     */
    public PathResolver(Path userHome, Path projectRoot) {
        this.userHome = Objects.requireNonNull(userHome, "userHome");
        this.projectRoot = projectRoot;
    }

    /**
     * Resolve a manifest entry to an absolute path.
     *
     * @param entry the manifest entry
     * @return absolute, normalised path
     * @throws ScaffoldException if the entry's scope does not match
     *                           the available roots or the dest form
     *                           is inconsistent with its scope
     */
    public Path resolve(ManifestEntry entry) {
        return resolveDest(entry.dest(), entry.scope());
    }

    /**
     * Resolve a bare {@code dest} string to an absolute path.
     *
     * <p>Used for lockfile entries that have no surviving manifest
     * counterpart (orphans) — the scope is inferred by the caller
     * from which lockfile the {@code dest} came out of.
     *
     * @param dest  the manifest {@code dest} string
     * @param scope the scope to resolve under
     * @return absolute, normalised path
     * @throws ScaffoldException if the dest form is inconsistent with
     *                           its scope or the required root is
     *                           unavailable
     */
    public Path resolveDest(String dest, ScaffoldScope scope) {
        if (scope == ScaffoldScope.USER) {
            if (!dest.startsWith("~/")) {
                throw new ScaffoldException(
                        "USER-scope entry '" + dest
                                + "' must start with '~/'");
            }
            return userHome.resolve(dest.substring(2)).normalize();
        }
        // PROJECT
        if (dest.startsWith("~/")) {
            throw new ScaffoldException(
                    "PROJECT-scope entry '" + dest
                            + "' must not start with '~/'");
        }
        if (projectRoot == null) {
            throw new ScaffoldException(
                    "PROJECT-scope entry '" + dest
                            + "' needs a projectRoot but none was "
                            + "configured");
        }
        String rel = dest.startsWith("{project.root}/")
                ? dest.substring("{project.root}/".length())
                : dest;
        return projectRoot.resolve(rel).normalize();
    }
}