Add the repo rule `http_jar`

Copybara Import from https://github.com/bazelbuild/rules_java/pull/238

BEGIN_PUBLIC
Add the repo rule `http_jar` (#238)

The repo rule `http_jar` currently lives in `@bazel_tools`, which results in an implicit dependency on `rules_java` because it creates a repo with a BUILD file that uses `java_import`. So this repo rule really belongs in `rules_java`.

This PR copies it over from `@bazel_tools//tools/build_defs/repo:http.bzl` with minimal edits.

Closes #238
END_PUBLIC

COPYBARA_INTEGRATE_REVIEW=https://github.com/bazelbuild/rules_java/pull/238 from bazelbuild:wyv-http-jar 74d325a744cd7333e8669685d3ea1d3112a5078c
PiperOrigin-RevId: 693270198
Change-Id: I44f6d60301f27db8515243af54f79cb0ab4a7c0c
diff --git a/.bazelci/presubmit.yml b/.bazelci/presubmit.yml
index f89bc94..8971a40 100644
--- a/.bazelci/presubmit.yml
+++ b/.bazelci/presubmit.yml
@@ -42,6 +42,8 @@
       - "//..."
       - "//:bin_deploy.jar"
       - "@rules_java//java/..."
+    test_targets:
+      - "//:MyTest"
   macos:
     name: "Bazel 7.x"
     bazel: "7.4.0"
@@ -67,6 +69,8 @@
       - "//..."
       - "//:bin_deploy.jar"
       - "@rules_java//java/..."
+    test_targets:
+      - "//:MyTest"
   macos_head:
     name: "Bazel@HEAD"
     bazel: last_green
@@ -94,6 +98,7 @@
     build_targets:
     - "//..."
     - "//:bin_deploy.jar"
+    - "-//:MyTest"
   macos_bazel6:
     name: "Bazel 6.x"
     bazel: 6.5.0
diff --git a/.bazelignore b/.bazelignore
new file mode 100644
index 0000000..457eb2a
--- /dev/null
+++ b/.bazelignore
@@ -0,0 +1 @@
+test/repo
diff --git a/java/http_jar.bzl b/java/http_jar.bzl
new file mode 100644
index 0000000..8865482
--- /dev/null
+++ b/java/http_jar.bzl
@@ -0,0 +1,200 @@
+"""The http_jar repo rule, for downloading jars over HTTP.
+
+### Setup
+
+To use this rule in a module extension, load it in your .bzl file and then call it from your
+extension's implementation function. For example:
+
+```python
+load("@rules_java//java:http_jar.bzl", "http_jar")
+
+def _my_extension_impl(mctx):
+  http_jar(name = "foo", urls = [...])
+
+my_extension = module_extension(implementation = _my_extension_impl)
+```
+
+Alternatively, you can directly call it your MODULE.bazel file with `use_repo_rule`:
+
+```python
+http_jar = use_repo_rule("@rules_java//java:http_jar.bzl", "http_jar")
+http_jar(name = "foo", urls = [...])
+```
+"""
+
+load("@bazel_tools//tools/build_defs/repo:cache.bzl", "CANONICAL_ID_DOC", "DEFAULT_CANONICAL_ID_ENV", "get_default_canonical_id")
+load("@bazel_tools//tools/build_defs/repo:utils.bzl", "get_auth", "update_attrs")
+
+_URL_DOC = """A URL to the jar that will be made available to Bazel.
+
+This must be a file, http or https URL. Redirections are followed.
+Authentication is not supported.
+
+More flexibility can be achieved by the urls parameter that allows
+to specify alternative URLs to fetch from."""
+
+_URLS_DOC = """A list of URLs to the jar that will be made available to Bazel.
+
+Each entry must be a file, http or https URL. Redirections are followed.
+Authentication is not supported.
+
+URLs are tried in order until one succeeds, so you should list local mirrors first.
+If all downloads fail, the rule will fail."""
+
+_AUTH_PATTERN_DOC = """An optional dict mapping host names to custom authorization patterns.
+
+If a URL's host name is present in this dict the value will be used as a pattern when
+generating the authorization header for the http request. This enables the use of custom
+authorization schemes used in a lot of common cloud storage providers.
+
+The pattern currently supports 2 tokens: <code>&lt;login&gt;</code> and
+<code>&lt;password&gt;</code>, which are replaced with their equivalent value
+in the netrc file for the same host name. After formatting, the result is set
+as the value for the <code>Authorization</code> field of the HTTP request.
+
+Example attribute and netrc for a http download to an oauth2 enabled API using a bearer token:
+
+<pre>
+auth_patterns = {
+    "storage.cloudprovider.com": "Bearer &lt;password&gt;"
+}
+</pre>
+
+netrc:
+
+<pre>
+machine storage.cloudprovider.com
+        password RANDOM-TOKEN
+</pre>
+
+The final HTTP request would have the following header:
+
+<pre>
+Authorization: Bearer RANDOM-TOKEN
+</pre>
+"""
+
+def _get_source_urls(ctx):
+    """Returns source urls provided via the url, urls attributes.
+
+    Also checks that at least one url is provided."""
+    if not ctx.attr.url and not ctx.attr.urls:
+        fail("At least one of url and urls must be provided")
+
+    source_urls = []
+    if ctx.attr.urls:
+        source_urls = ctx.attr.urls
+    if ctx.attr.url:
+        source_urls = [ctx.attr.url] + source_urls
+    return source_urls
+
+def _update_integrity_attr(ctx, attrs, download_info):
+    # We don't need to override the integrity attribute if sha256 is already specified.
+    integrity_override = {} if ctx.attr.sha256 else {"integrity": download_info.integrity}
+    return update_attrs(ctx.attr, attrs.keys(), integrity_override)
+
+_HTTP_JAR_BUILD = """\
+load("{java_import_bzl}", "java_import")
+
+java_import(
+  name = 'jar',
+  jars = ["{file_name}"],
+  visibility = ['//visibility:public'],
+)
+
+filegroup(
+  name = 'file',
+  srcs = ["{file_name}"],
+  visibility = ['//visibility:public'],
+)
+
+"""
+
+def _http_jar_impl(ctx):
+    """Implementation of the http_jar rule."""
+    source_urls = _get_source_urls(ctx)
+    downloaded_file_name = ctx.attr.downloaded_file_name
+    download_info = ctx.download(
+        source_urls,
+        "jar/" + downloaded_file_name,
+        ctx.attr.sha256,
+        canonical_id = ctx.attr.canonical_id or get_default_canonical_id(ctx, source_urls),
+        auth = get_auth(ctx, source_urls),
+        integrity = ctx.attr.integrity,
+    )
+    ctx.file("jar/BUILD", _HTTP_JAR_BUILD.format(
+        java_import_bzl = str(Label("//java:java_import.bzl")),
+        file_name = downloaded_file_name,
+    ))
+
+    return _update_integrity_attr(ctx, _http_jar_attrs, download_info)
+
+_http_jar_attrs = {
+    "sha256": attr.string(
+        doc = """The expected SHA-256 of the jar downloaded.
+
+This must match the SHA-256 of the jar downloaded. _It is a security risk
+to omit the SHA-256 as remote files can change._ At best omitting this
+field will make your build non-hermetic. It is optional to make development
+easier but either this attribute or `integrity` should be set before shipping.""",
+    ),
+    "integrity": attr.string(
+        doc = """Expected checksum in Subresource Integrity format of the jar downloaded.
+
+This must match the checksum of the file downloaded. _It is a security risk
+to omit the checksum as remote files can change._ At best omitting this
+field will make your build non-hermetic. It is optional to make development
+easier but either this attribute or `sha256` should be set before shipping.""",
+    ),
+    "canonical_id": attr.string(
+        doc = CANONICAL_ID_DOC,
+    ),
+    "url": attr.string(doc = _URL_DOC + "\n\nThe URL must end in `.jar`."),
+    "urls": attr.string_list(doc = _URLS_DOC + "\n\nAll URLs must end in `.jar`."),
+    "netrc": attr.string(
+        doc = "Location of the .netrc file to use for authentication",
+    ),
+    "auth_patterns": attr.string_dict(
+        doc = _AUTH_PATTERN_DOC,
+    ),
+    "downloaded_file_name": attr.string(
+        default = "downloaded.jar",
+        doc = "Filename assigned to the jar downloaded",
+    ),
+}
+
+http_jar = repository_rule(
+    implementation = _http_jar_impl,
+    attrs = _http_jar_attrs,
+    environ = [DEFAULT_CANONICAL_ID_ENV],
+    doc =
+        """Downloads a jar from a URL and makes it available as java_import
+
+Downloaded files must have a .jar extension.
+
+Examples:
+  Suppose the current repository contains the source code for a chat program, rooted at the
+  directory `~/chat-app`. It needs to depend on an SSL library which is available from
+  `http://example.com/openssl-0.2.jar`.
+
+  Targets in the `~/chat-app` repository can depend on this target if the following lines are
+  added to `~/chat-app/MODULE.bazel`:
+
+  ```python
+  http_jar = use_repo_rule("@rules_java//java:http_jar.bzl", "http_jar")
+
+  http_jar(
+      name = "my_ssl",
+      url = "http://example.com/openssl-0.2.jar",
+      sha256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
+  )
+  ```
+
+  Targets would specify `@my_ssl//jar` as a dependency to depend on this jar.
+
+  You may also reference files on the current system (localhost) by using "file:///path/to/file"
+  if you are on Unix-based systems. If you're on Windows, use "file:///c:/path/to/file". In both
+  examples, note the three slashes (`/`) -- the first two slashes belong to `file://` and the third
+  one belongs to the absolute path to the file.
+""",
+)
diff --git a/test/BUILD.bazel b/test/BUILD.bazel
index cea33a5..00506f8 100644
--- a/test/BUILD.bazel
+++ b/test/BUILD.bazel
@@ -1,6 +1,3 @@
-load("@bazel_skylib//rules:diff_test.bzl", "diff_test")
-load("@rules_shell//shell:sh_test.bzl", "sh_test")
-
 # Copyright 2024 The Bazel Authors. All rights reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
@@ -14,6 +11,9 @@
 # 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.
+
+load("@bazel_skylib//rules:diff_test.bzl", "diff_test")
+load("@rules_shell//shell:sh_test.bzl", "sh_test")
 load("//java:repositories.bzl", "JAVA_TOOLS_CONFIG", "REMOTE_JDK_CONFIGS")
 load(":check_remotejdk_configs_match.bzl", "validate_configs")
 
diff --git a/test/repo/BUILD.bazel b/test/repo/BUILD.bazel
index 6b7da40..20deeae 100644
--- a/test/repo/BUILD.bazel
+++ b/test/repo/BUILD.bazel
@@ -1,9 +1,9 @@
-load("@rules_java//java:defs.bzl", "java_binary", "java_library")  # copybara-use-repo-external-label
+load("@rules_java//java:defs.bzl", "java_binary", "java_library", "java_test")  # copybara-use-repo-external-label
 load("@rules_java//toolchains:default_java_toolchain.bzl", "default_java_toolchain")  # copybara-use-repo-external-label
 
 java_library(
     name = "lib",
-    srcs = glob(["src/*.java"]),
+    srcs = ["src/Main.java"],
 )
 
 java_binary(
@@ -12,6 +12,12 @@
     runtime_deps = [":lib"],
 )
 
+java_test(
+    name = "MyTest",
+    srcs = ["src/MyTest.java"],
+    deps = ["@my_jar//jar"],
+)
+
 default_java_toolchain(
     name = "my_funky_toolchain",
 )
diff --git a/test/repo/MODULE.bazel b/test/repo/MODULE.bazel
index c9729b4..5a4d611 100644
--- a/test/repo/MODULE.bazel
+++ b/test/repo/MODULE.bazel
@@ -6,6 +6,13 @@
     urls = ["file:///tmp/rules_java-HEAD.tar.gz"],
 )
 
+http_jar = use_repo_rule("@rules_java//java:http_jar.bzl", "http_jar")
+
+http_jar(
+    name = "my_jar",
+    urls = ["file:///tmp/my_jar.jar"],
+)
+
 java_toolchains = use_extension("@rules_java//java:extensions.bzl", "toolchains")
 use_repo(
     java_toolchains,
diff --git a/test/repo/setup.sh b/test/repo/setup.sh
index 4c268fc..7e3b369 100644
--- a/test/repo/setup.sh
+++ b/test/repo/setup.sh
@@ -1,5 +1,7 @@
 #!/usr/bin/env bash
 
 cd ../../
-bazel build //distro:all
-cp -f bazel-bin/distro/rules_java-*.tar.gz /tmp/rules_java-HEAD.tar.gz
\ No newline at end of file
+bazel build //distro:all //test/testdata:my_jar
+cp -f bazel-bin/distro/rules_java-*.tar.gz /tmp/rules_java-HEAD.tar.gz
+cp -f bazel-bin/test/testdata/libmy_jar.jar /tmp/my_jar.jar
+
diff --git a/test/repo/src/MyTest.java b/test/repo/src/MyTest.java
new file mode 100644
index 0000000..2de08a0
--- /dev/null
+++ b/test/repo/src/MyTest.java
@@ -0,0 +1,15 @@
+import static org.junit.Assert.assertEquals;
+
+import mypackage.MyLib;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class MyTest {
+  @Test
+  public void main() {
+    assertEquals(MyLib.myStr(), "my_string");
+  }
+}
+
diff --git a/test/testdata/BUILD.bazel b/test/testdata/BUILD.bazel
new file mode 100644
index 0000000..ffcc5c3
--- /dev/null
+++ b/test/testdata/BUILD.bazel
@@ -0,0 +1,7 @@
+load("//java:java_library.bzl", "java_library")
+
+# Make a sample jar for the http_jar test.
+java_library(
+    name = "my_jar",
+    srcs = ["MyLib.java"],
+)
diff --git a/test/testdata/MyLib.java b/test/testdata/MyLib.java
new file mode 100644
index 0000000..48428f1
--- /dev/null
+++ b/test/testdata/MyLib.java
@@ -0,0 +1,9 @@
+package mypackage;
+
+/** A simple library for the http_jar test. */
+public class MyLib {
+  public static String myStr() {
+    return "my_string";
+  }
+}
+