gnodet-bot commented on code in PR #13722:
URL: https://github.com/apache/maven/pull/13722#discussion_r4461346451


##########
api/maven-api-core/src/main/java/org/apache/maven/api/services/Sources.java:
##########
@@ -94,6 +98,165 @@ public static ModelSource resolvedSource(@Nonnull Path 
path, @Nullable String mo
         return new ResolvedPathSource(requireNonNull(path, "path"), 
path.toString(), modelId);
     }
 
+    /**
+     * Creates a new source backed by a URL, with an optional model identifier.
+     * <p>
+     * For hierarchical URLs (e.g. {@code file:}, {@code http:}, {@code 
https:}), sibling
+     * resolution via {@link Source#resolve(String)} is supported through URI 
resolution.
+     * For opaque URLs (e.g. {@code jar:} classpath entries), sibling 
resolution returns
+     * {@code null} since there is no meaningful "sibling" concept.
+     *
+     * @param url the URL to read from, must not be {@code null}
+     * @param modelId the modelId (groupId:artifactId:version coordinates), 
may be {@code null}
+     * @return a new ModelSource instance backed by the URL
+     * @throws NullPointerException if url is null
+     * @since 4.1.0
+     */
+    @Nonnull
+    public static ModelSource urlSource(@Nonnull URL url, @Nullable String 
modelId) {
+        return new UrlModelSource(requireNonNull(url, "url"), modelId);
+    }
+
+    /**
+     * Implementation of {@link ModelSource} backed by a URL (e.g. a classpath 
resource or file URL).
+     * <p>
+     * Sibling resolution is supported for hierarchical URLs (e.g. {@code 
file:}, {@code http:},
+     * {@code https:}) via URI relative resolution. For opaque URLs (e.g. 
{@code jar:} entries),
+     * resolution always returns {@code null}.
+     * <p>
+     * For {@code file:} URLs, the {@link ModelSource#resolve(ModelLocator, 
String)} method
+     * additionally delegates to the locator to find sibling POM files on the 
filesystem,
+     * mirroring the behavior of {@link BuildPathSource}.
+     */
+    static class UrlModelSource implements ModelSource {
+        private final URL url;
+
+        @Nullable
+        private final String modelId;
+
+        UrlModelSource(URL url, String modelId) {
+            this.url = requireNonNull(url, "url");
+            this.modelId = modelId;
+        }
+
+        @Override
+        @Nullable
+        public Path getPath() {
+            return null;
+        }
+
+        @Override
+        @Nonnull
+        public InputStream openStream() throws IOException {
+            return url.openStream();
+        }
+
+        @Override
+        @Nonnull
+        public String getLocation() {
+            return url.toExternalForm();
+        }
+
+        /**
+         * Resolves a sibling source relative to this URL source.
+         * <p>
+         * For hierarchical URLs (non-opaque, e.g. {@code file:}, {@code 
http:}), resolves
+         * the relative path using URI resolution against the parent 
"directory" of the URL.
+         * For opaque URLs (e.g. {@code jar:} entries), returns {@code null}.
+         *
+         * @param relative the relative path to resolve
+         * @return a new {@link Source} for the resolved URL, or {@code null} 
if not resolvable
+         */
+        @Override
+        @Nullable
+        public Source resolve(@Nonnull String relative) {
+            try {
+                URI uri = url.toURI();
+                if (uri.isOpaque()) {
+                    // Opaque URIs (jar:, etc.) cannot resolve siblings
+                    return null;
+                }
+                // Resolve against the parent "directory" — strip the last 
path segment first
+                URI base = uri.getPath().endsWith("/") ? uri : 
uri.resolve(".");
+                URI resolved = base.resolve(relative);
+                return new UrlModelSource(resolved.toURL(), null);
+            } catch (URISyntaxException | IllegalArgumentException | 
java.net.MalformedURLException e) {
+                return null;
+            }
+        }
+
+        /**
+         * Resolves a sibling {@link ModelSource} relative to this URL source, 
using the locator
+         * to find the actual POM file.
+         * <p>
+         * For {@code file:} URLs, the resolved URI is converted to a 
filesystem path and passed
+         * to the locator (mirroring {@link BuildPathSource} behavior). For 
other hierarchical
+         * URLs, the resolved URL is wrapped directly. For opaque URLs, 
returns {@code null}.
+         *
+         * @param modelLocator the locator used to find the POM file
+         * @param relative the relative path to resolve
+         * @return a resolved {@link ModelSource}, or {@code null} if not 
resolvable
+         */
+        @Override
+        @Nullable
+        public ModelSource resolve(@Nonnull ModelSource.ModelLocator 
modelLocator, @Nonnull String relative) {
+            try {
+                URI uri = url.toURI();
+                if (uri.isOpaque()) {
+                    return null;
+                }
+                URI base = uri.getPath().endsWith("/") ? uri : 
uri.resolve(".");
+                URI resolved = base.resolve(relative);
+                if ("file".equalsIgnoreCase(resolved.getScheme())) {
+                    // For file: URLs, use the locator to find the actual POM 
(honours pomFile discovery)
+                    String norm = relative.replace('\\', 
File.separatorChar).replace('/', File.separatorChar);
+                    Path siblingPath;
+                    try {
+                        Path parentPath = Path.of(uri).getParent();
+                        if (parentPath == null) {
+                            return null;
+                        }
+                        siblingPath = parentPath.resolve(norm);
+                    } catch (InvalidPathException e) {
+                        return null;
+                    }
+                    Path pomPath = modelLocator.locateExistingPom(siblingPath);
+                    if (pomPath != null) {
+                        return new UrlModelSource(pomPath.toUri().toURL(), 
null);
+                    }
+                    return null;
+                }
+                return new UrlModelSource(resolved.toURL(), null);
+            } catch (URISyntaxException | IllegalArgumentException | 
java.net.MalformedURLException e) {
+                return null;
+            }
+        }
+
+        @Override
+        @Nullable
+        public String getModelId() {
+            return modelId;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            return o == this
+                    || o instanceof UrlModelSource that

Review Comment:
   ⚠️ **`URL.equals()` / `URL.hashCode()` can trigger DNS resolution for 
`http:` / `https:` URLs**
   
   `java.net.URL.equals()` and `hashCode()` resolve hostnames to IP addresses 
when comparing non-`file:`/non-`jar:` URLs. From the Javadoc:
   
   > Two hosts are considered equivalent if both host names can be resolved 
into the same IP addresses; this operation requires network access and is 
inconsistent with virtual hosting in HTTP.
   
   This means `Objects.equals(url, that.url)` and `Objects.hash(url, ...)` in 
both methods can block on a network call if `url` uses an `http:` or `https:` 
scheme. While the primary use-case today is the `jar:` super POM (opaque — 
safe), the new `resolve()` methods produce `http:`/`https:` `UrlModelSource` 
instances, so equals/hashCode on those will trigger DNS.
   
   Fix both methods to compare by external form (string — deterministic, 
network-free):
   
   ```suggestion
           @Override
           public boolean equals(Object o) {
               return o == this
                       || o instanceof UrlModelSource that
                               && Objects.equals(url.toExternalForm(), 
that.url.toExternalForm())
                               && Objects.equals(modelId, that.modelId);
           }
   ```
   
   For `hashCode()`, update it analogously to use `url.toExternalForm()` as 
well (a single-line change — `Objects.hash(url.toExternalForm(), modelId)`).
   
   Note: `URI.equals()` is also network-free and would work if `url` is stored 
as a `URI` internally, but converting the `URL` field to `URI` is a larger 
change.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to