Downloader rewriter config has all_blocked_message

This allows for the config author to specify a useful error message to
adorn the user-facing error, so that if the user is unaware or forgets
about the config, they get a more clear error message than:
```
ERROR: java.io.IOException: Error downloading [] to /some/path
```
or
```
Error in download: java.io.IOException: Cache miss and no url specified
```

Currently there is a log line like:
```
INFO: Rewritten [https://doesnotexist.com/beep] as []
```

printed, but this is often quite far from the error - this puts all the
information in one place.

If you don't configure an all_blocked_message, this now looks like:

```
ERROR: java.io.IOException: Cache miss and no url specified - Configured URL rewriter blocked all URLs: [https://doesnotexist.com/beep]
```

And if you do, it will look like something like:

```
ERROR: java.io.IOException: Cache miss and no url specified - Configured URL rewriter blocked all URLs: [https://doesnotexist.com/beep] - See https://mycorp.com/bazel-rewriter-issue for more details.
```

Closes #12997.

PiperOrigin-RevId: 359730842
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/repository/downloader/DownloadManager.java b/src/main/java/com/google/devtools/build/lib/bazel/repository/downloader/DownloadManager.java
index a117b7d..a8d0b1f 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/repository/downloader/DownloadManager.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/repository/downloader/DownloadManager.java
@@ -33,6 +33,7 @@
 import java.net.URL;
 import java.util.List;
 import java.util.Map;
+import javax.annotation.Nullable;
 
 /**
  * Bazel file downloader.
@@ -85,7 +86,7 @@
    * @throws InterruptedException if this thread is being cast into oblivion
    */
   public Path download(
-      List<URL> urls,
+      List<URL> originalUrls,
       Map<URI, Map<String, String>> authHeaders,
       Optional<Checksum> checksum,
       String canonicalId,
@@ -99,20 +100,21 @@
       throw new InterruptedException();
     }
 
+    List<URL> rewrittenUrls = originalUrls;
     if (rewriter != null) {
-      urls = rewriter.amend(urls);
+      rewrittenUrls = rewriter.amend(originalUrls);
     }
 
     URL mainUrl; // The "main" URL for this request
     // Used for reporting only and determining the file name only.
-    if (urls.isEmpty()) {
+    if (rewrittenUrls.isEmpty()) {
       if (type.isPresent() && !Strings.isNullOrEmpty(type.get())) {
         mainUrl = new URL("http://nonexistent.example.org/cacheprobe." + type.get());
       } else {
         mainUrl = new URL("http://nonexistent.example.org/cacheprobe");
       }
     } else {
-      mainUrl = urls.get(0);
+      mainUrl = rewrittenUrls.get(0);
     }
     Path destination = getDownloadDestination(mainUrl, type, output);
     ImmutableSet<String> candidateFileNames = getCandidateFileNames(mainUrl, destination);
@@ -153,8 +155,13 @@
         }
       }
 
-      if (urls.isEmpty()) {
-        throw new IOException("Cache miss and no url specified");
+      if (rewrittenUrls.isEmpty()) {
+        StringBuilder message = new StringBuilder("Cache miss and no url specified");
+        if (!originalUrls.isEmpty()) {
+          message.append(" - ");
+          message.append(getRewriterBlockedAllUrlsMessage(originalUrls));
+        }
+        throw new IOException(message.toString());
       }
 
       for (Path dir : distdir) {
@@ -204,9 +211,20 @@
           String.format("Failed to download repo %s: download is disabled.", repo));
     }
 
+    if (rewrittenUrls.isEmpty() && !originalUrls.isEmpty()) {
+      throw new IOException(getRewriterBlockedAllUrlsMessage(originalUrls));
+    }
+
     try {
       downloader.download(
-          urls, authHeaders, checksum, canonicalId, destination, eventHandler, clientEnv, type);
+          rewrittenUrls,
+          authHeaders,
+          checksum,
+          canonicalId,
+          destination,
+          eventHandler,
+          clientEnv,
+          type);
     } catch (InterruptedIOException e) {
       throw new InterruptedException(e.getMessage());
     }
@@ -216,12 +234,26 @@
           checksum.get().toString(), destination, checksum.get().getKeyType(), canonicalId);
     } else if (repositoryCache.isEnabled()) {
       String newSha256 = repositoryCache.put(destination, KeyType.SHA256, canonicalId);
-      eventHandler.handle(Event.info("SHA256 (" + urls.get(0) + ") = " + newSha256));
+      eventHandler.handle(Event.info("SHA256 (" + rewrittenUrls.get(0) + ") = " + newSha256));
     }
 
     return destination;
   }
 
+  @Nullable
+  private String getRewriterBlockedAllUrlsMessage(List<URL> originalUrls) {
+    if (rewriter == null) {
+      return null;
+    }
+    StringBuilder message = new StringBuilder("Configured URL rewriter blocked all URLs: ");
+    message.append(originalUrls);
+    String rewriterMessage = rewriter.getAllBlockedMessage();
+    if (rewriterMessage != null) {
+      message.append(" - ").append(rewriterMessage);
+    }
+    return message.toString();
+  }
+
   private Path getDownloadDestination(URL url, Optional<String> type, Path output) {
     if (!type.isPresent()) {
       return output;
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/repository/downloader/UrlRewriter.java b/src/main/java/com/google/devtools/build/lib/bazel/repository/downloader/UrlRewriter.java
index dfeee88..a081f43 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/repository/downloader/UrlRewriter.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/repository/downloader/UrlRewriter.java
@@ -40,6 +40,7 @@
 import java.util.function.Function;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
+import javax.annotation.Nullable;
 
 /**
  * Helper class for taking URLs and converting them according to an optional config specified by
@@ -196,4 +197,9 @@
             })
         .collect(toImmutableList());
   }
+
+  @Nullable
+  public String getAllBlockedMessage() {
+    return config.getAllBlockedMessage();
+  }
 }
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/repository/downloader/UrlRewriterConfig.java b/src/main/java/com/google/devtools/build/lib/bazel/repository/downloader/UrlRewriterConfig.java
index 322cf11..ad3b170 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/repository/downloader/UrlRewriterConfig.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/repository/downloader/UrlRewriterConfig.java
@@ -25,6 +25,7 @@
 import java.util.Map;
 import java.util.Set;
 import java.util.regex.Pattern;
+import javax.annotation.Nullable;
 import net.starlark.java.syntax.Location;
 
 /**
@@ -36,11 +37,15 @@
  *   <li>{@code block hostName} Will block access to the given host and subdomains
  *   <li>{@code rewrite pattern pattern} Rewrite a URL using the given pattern. Back references are
  *       numbered from `$1`
+ *   <li>{@code all_blocked_message message which may contain spaces} If the rewriter causes all
+ *       URLs for a particular resource to be blocked, this informational message will be rendered
+ *       to the user. This directive may only be present at most once.
  * </ul>
  *
  * The directives are applied in the order `rewrite, allow, block'. An example config may look like:
  *
  * <pre>
+ *     all_blocked_message See mycorp.com/blocked-bazel-fetches for more information.
  *     block mvnrepository.com
  *     block maven-central.storage.googleapis.com
  *     block gitblit.github.io
@@ -62,6 +67,7 @@
 
   private static final Splitter SPLITTER =
       Splitter.onPattern("\\s+").omitEmptyStrings().trimResults();
+  private static final String ALL_BLOCKED_MESSAGE_DIRECTIVE = "all_blocked_message";
 
   // A set of domain names that should be accessible.
   private final Set<String> allowList;
@@ -69,6 +75,8 @@
   private final Set<String> blockList;
   // A set of patterns matching "everything in the url after the scheme" to rewrite rules.
   private final ImmutableMultimap<Pattern, String> rewrites;
+  // Message to display if the rewriter caused all URLs to be blocked.
+  @Nullable private final String allBlockedMessage;
 
   /**
    * Constructor to use. The {@code config} will be read to completion.
@@ -81,6 +89,7 @@
     ImmutableSet.Builder<String> allowList = ImmutableSet.builder();
     ImmutableSet.Builder<String> blockList = ImmutableSet.builder();
     ImmutableMultimap.Builder<Pattern, String> rewrites = ImmutableMultimap.builder();
+    String allBlockedMessage = null;
 
     try (BufferedReader reader = new BufferedReader(config)) {
       int lineNumber = 1;
@@ -125,6 +134,18 @@
             rewrites.put(Pattern.compile(parts.get(1)), parts.get(2));
             break;
 
+          case ALL_BLOCKED_MESSAGE_DIRECTIVE:
+            if (parts.size() == 1) {
+              throw new UrlRewriterParseException(
+                  "all_blocked_message must be followed by a message", location);
+            }
+            if (allBlockedMessage != null) {
+              throw new UrlRewriterParseException(
+                  "At most one all_blocked_message directive is allowed", location);
+            }
+            allBlockedMessage = line.substring(ALL_BLOCKED_MESSAGE_DIRECTIVE.length() + 1);
+            break;
+
           default:
             throw new UrlRewriterParseException("Unable to parse: " + line, location);
         }
@@ -136,6 +157,7 @@
     this.allowList = allowList.build();
     this.blockList = blockList.build();
     this.rewrites = rewrites.build();
+    this.allBlockedMessage = allBlockedMessage;
   }
 
   /** Returns all {@code allow} directives. */
@@ -155,4 +177,9 @@
   public Map<Pattern, Collection<String>> getRewrites() {
     return rewrites.asMap();
   }
+
+  @Nullable
+  public String getAllBlockedMessage() {
+    return allBlockedMessage;
+  }
 }
diff --git a/src/test/java/com/google/devtools/build/lib/bazel/repository/downloader/UrlRewriterTest.java b/src/test/java/com/google/devtools/build/lib/bazel/repository/downloader/UrlRewriterTest.java
index 10ec84e..805cf01 100644
--- a/src/test/java/com/google/devtools/build/lib/bazel/repository/downloader/UrlRewriterTest.java
+++ b/src/test/java/com/google/devtools/build/lib/bazel/repository/downloader/UrlRewriterTest.java
@@ -185,4 +185,31 @@
       assertThat(e.getLocation()).isEqualTo(Location.fromFileLineColumn("/some/file", 2, 0));
     }
   }
+
+  @Test
+  public void noAllBlockedMessage() throws Exception {
+    String config = "";
+    UrlRewriterConfig munger = new UrlRewriterConfig("/some/file", new StringReader(config));
+    assertThat(munger.getAllBlockedMessage()).isNull();
+  }
+
+  @Test
+  public void singleAllBlockedMessage() throws Exception {
+    String config =
+        "all_blocked_message I'm sorry Dave, I'm afraid I can't do that.\n" + "allow *\n";
+    UrlRewriterConfig munger = new UrlRewriterConfig("/some/file", new StringReader(config));
+    assertThat(munger.getAllBlockedMessage())
+        .isEqualTo("I'm sorry Dave, I'm afraid I can't do that.");
+  }
+
+  @Test
+  public void multipleAllBlockedMessage() throws Exception {
+    String config = "all_blocked_message one\n" + "block *\n" + "all_blocked_message two\n";
+    try {
+      new UrlRewriterConfig("/some/file", new StringReader(config));
+      fail();
+    } catch (UrlRewriterParseException e) {
+      assertThat(e.getLocation()).isEqualTo(Location.fromFileLineColumn("/some/file", 3, 0));
+    }
+  }
 }