Move PackageIdentifier to cmdline
This is necessary to have TargetResolver depend on it without making it depend
on the packages target. First step of #389.
--
MOS_MIGRATED_REVID=101790345
diff --git a/src/main/java/com/google/devtools/build/lib/cmdline/PackageIdentifier.java b/src/main/java/com/google/devtools/build/lib/cmdline/PackageIdentifier.java
new file mode 100644
index 0000000..034df83
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/cmdline/PackageIdentifier.java
@@ -0,0 +1,352 @@
+// Copyright 2015 Google Inc. All rights reserved.
+//
+// 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.google.devtools.build.lib.cmdline;
+
+import com.google.common.base.Preconditions;
+import com.google.common.base.Throwables;
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.google.common.collect.ComparisonChain;
+import com.google.devtools.build.lib.util.StringCanonicalizer;
+import com.google.devtools.build.lib.util.StringUtilities;
+import com.google.devtools.build.lib.vfs.Canonicalizer;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.ObjectStreamException;
+import java.io.Serializable;
+import java.util.Objects;
+import java.util.concurrent.ExecutionException;
+
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * Uniquely identifies a package, given a repository name and a package's path fragment.
+ *
+ * <p>The repository the build is happening in is the <i>default workspace</i>, and is identified
+ * by the workspace name "". Other repositories can be named in the WORKSPACE file. These
+ * workspaces are prefixed by {@literal @}.</p>
+ */
+@Immutable
+public final class PackageIdentifier implements Comparable<PackageIdentifier>, Serializable {
+
+ /**
+ * A human-readable name for the repository.
+ */
+ public static final class RepositoryName {
+ private static final LoadingCache<String, RepositoryName> repositoryNameCache =
+ CacheBuilder.newBuilder()
+ .weakValues()
+ .build(
+ new CacheLoader<String, RepositoryName> () {
+ @Override
+ public RepositoryName load(String name) throws TargetParsingException {
+ String errorMessage = validate(name);
+ if (errorMessage != null) {
+ errorMessage = "invalid repository name '"
+ + StringUtilities.sanitizeControlChars(name) + "': " + errorMessage;
+ throw new TargetParsingException(errorMessage);
+ }
+ return new RepositoryName(StringCanonicalizer.intern(name));
+ }
+ });
+
+ /**
+ * Makes sure that name is a valid repository name and creates a new RepositoryName using it.
+ * @throws TargetParsingException if the name is invalid.
+ */
+ public static RepositoryName create(String name) throws TargetParsingException {
+ try {
+ return repositoryNameCache.get(name);
+ } catch (ExecutionException e) {
+ Throwables.propagateIfInstanceOf(e.getCause(), TargetParsingException.class);
+ throw new IllegalStateException("Failed to create RepositoryName from " + name, e);
+ }
+ }
+
+ private final String name;
+
+ private RepositoryName(String name) {
+ this.name = name;
+ }
+
+ /**
+ * Performs validity checking. Returns null on success, an error message otherwise.
+ */
+ private static String validate(String name) {
+ if (name.isEmpty()) {
+ return null;
+ }
+
+ if (!name.startsWith("@")) {
+ return "workspace name must start with '@'";
+ }
+
+ // "@" isn't a valid workspace name.
+ if (name.length() == 1) {
+ return "empty workspace name";
+ }
+
+ // Check for any character outside of [/0-9A-Z_a-z-._]. Try to evaluate the
+ // conditional quickly (by looking in decreasing order of character class
+ // likelihood).
+ if (name.startsWith("@/") || name.endsWith("/")) {
+ return "workspace names cannot start nor end with '/'";
+ } else if (name.contains("//")) {
+ return "workspace names cannot contain multiple '/'s in a row";
+ }
+
+ for (int i = name.length() - 1; i >= 1; --i) {
+ char c = name.charAt(i);
+ if ((c < 'a' || c > 'z') && c != '_' && c != '-' && c != '/' && c != '.'
+ && (c < '0' || c > '9') && (c < 'A' || c > 'Z')) {
+ return "workspace names may contain only A-Z, a-z, 0-9, '-', '_', '.', and '/'";
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns the repository name without the leading "{@literal @}". For the default repository,
+ * returns "".
+ */
+ public String strippedName() {
+ if (name.isEmpty()) {
+ return name;
+ }
+ return name.substring(1);
+ }
+
+ /**
+ * Returns if this is the default repository, that is, {@link #name} is "".
+ */
+ public boolean isDefault() {
+ return name.isEmpty();
+ }
+
+ /**
+ * Returns the repository name, with leading "{@literal @}" (or "" for the default repository).
+ */
+ // TODO(bazel-team): Use this over toString()- easier to track its usage.
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Returns the repository name, with leading "{@literal @}" (or "" for the default repository).
+ */
+ @Override
+ public String toString() {
+ return name;
+ }
+
+ @Override
+ public boolean equals(Object object) {
+ if (this == object) {
+ return true;
+ }
+ if (!(object instanceof RepositoryName)) {
+ return false;
+ }
+ return name.equals(((RepositoryName) object).name);
+ }
+
+ @Override
+ public int hashCode() {
+ return name.hashCode();
+ }
+ }
+
+ public static final String DEFAULT_REPOSITORY = "";
+ public static final RepositoryName DEFAULT_REPOSITORY_NAME;
+
+ static {
+ try {
+ DEFAULT_REPOSITORY_NAME = RepositoryName.create(DEFAULT_REPOSITORY);
+ } catch (TargetParsingException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ /**
+ * Helper for serializing PackageIdentifiers.
+ *
+ * <p>PackageIdentifier's field should be final, but then it couldn't be deserialized. This
+ * allows the fields to be deserialized and copied into a new PackageIdentifier.</p>
+ */
+ private static final class SerializationProxy implements Serializable {
+ PackageIdentifier packageId;
+
+ public SerializationProxy(PackageIdentifier packageId) {
+ this.packageId = packageId;
+ }
+
+ private void writeObject(ObjectOutputStream out) throws IOException {
+ out.writeObject(packageId.repository.toString());
+ out.writeObject(packageId.pkgName);
+ }
+
+ private void readObject(ObjectInputStream in)
+ throws IOException, ClassNotFoundException {
+ try {
+ packageId = new PackageIdentifier((String) in.readObject(), (PathFragment) in.readObject());
+ } catch (TargetParsingException e) {
+ throw new IOException("Error serializing package identifier: " + e.getMessage());
+ }
+ }
+
+ @SuppressWarnings("unused")
+ private void readObjectNoData() throws ObjectStreamException {
+ }
+
+ private Object readResolve() {
+ return packageId;
+ }
+ }
+
+ // Temporary factory for identifiers without explicit repositories.
+ // TODO(bazel-team): remove all usages of this.
+ public static PackageIdentifier createInDefaultRepo(String name) {
+ return createInDefaultRepo(new PathFragment(name));
+ }
+
+ public static PackageIdentifier createInDefaultRepo(PathFragment name) {
+ try {
+ return new PackageIdentifier(DEFAULT_REPOSITORY, name);
+ } catch (TargetParsingException e) {
+ throw new IllegalArgumentException("could not create package identifier for " + name
+ + ": " + e.getMessage());
+ }
+ }
+
+ /**
+ * The identifier for this repository. This is either "" or prefixed with an "@",
+ * e.g., "@myrepo".
+ */
+ private final RepositoryName repository;
+
+ /** The name of the package. Canonical (i.e. x.equals(y) <=> x==y). */
+ private final PathFragment pkgName;
+
+ public PackageIdentifier(String repository, PathFragment pkgName) throws TargetParsingException {
+ this(RepositoryName.create(repository), pkgName);
+ }
+
+ public PackageIdentifier(RepositoryName repository, PathFragment pkgName) {
+ Preconditions.checkNotNull(repository);
+ Preconditions.checkNotNull(pkgName);
+ this.repository = repository;
+ this.pkgName = Canonicalizer.fragments().intern(pkgName.normalize());
+ }
+
+ public static PackageIdentifier parse(String input) throws TargetParsingException {
+ String repo;
+ String packageName;
+ int packageStartPos = input.indexOf("//");
+ if (packageStartPos > 0) {
+ repo = input.substring(0, packageStartPos);
+ packageName = input.substring(packageStartPos + 2);
+ } else if (packageStartPos == 0) {
+ repo = PackageIdentifier.DEFAULT_REPOSITORY;
+ packageName = input.substring(2);
+ } else {
+ repo = PackageIdentifier.DEFAULT_REPOSITORY;
+ packageName = input;
+ }
+
+ String error = RepositoryName.validate(repo);
+ if (error != null) {
+ throw new TargetParsingException(error);
+ }
+
+ error = LabelValidator.validatePackageName(packageName);
+ if (error != null) {
+ throw new TargetParsingException(error);
+ }
+
+ return new PackageIdentifier(repo, new PathFragment(packageName));
+ }
+
+ private Object writeReplace() throws ObjectStreamException {
+ return new SerializationProxy(this);
+ }
+
+ private void readObject(ObjectInputStream in)
+ throws IOException, ClassNotFoundException {
+ throw new IOException("Serialization is allowed only by proxy");
+ }
+
+ @SuppressWarnings("unused")
+ private void readObjectNoData() throws ObjectStreamException {
+ }
+
+ public RepositoryName getRepository() {
+ return repository;
+ }
+
+ public PathFragment getPackageFragment() {
+ return pkgName;
+ }
+
+ /**
+ * Returns a relative path that should be unique across all remote and packages, based on the
+ * repository and package names.
+ */
+ public PathFragment getPathFragment() {
+ return repository.isDefault() ? pkgName
+ : new PathFragment("external").getRelative(repository.strippedName())
+ .getRelative(pkgName);
+ }
+
+ /**
+ * Returns the name of this package.
+ *
+ * <p>There are certain places that expect the path fragment as the package name ('foo/bar') as a
+ * package identifier. This isn't specific enough for packages in other repositories, so their
+ * stringified version is '@baz//foo/bar'.</p>
+ */
+ @Override
+ public String toString() {
+ return (repository.isDefault() ? "" : repository + "//") + pkgName;
+ }
+
+ @Override
+ public boolean equals(Object object) {
+ if (this == object) {
+ return true;
+ }
+ if (!(object instanceof PackageIdentifier)) {
+ return false;
+ }
+ PackageIdentifier that = (PackageIdentifier) object;
+ return pkgName.equals(that.pkgName) && repository.equals(that.repository);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(repository, pkgName);
+ }
+
+ @Override
+ public int compareTo(PackageIdentifier that) {
+ return ComparisonChain.start()
+ .compare(repository.toString(), that.repository.toString())
+ .compare(pkgName, that.pkgName)
+ .result();
+ }
+}