deepakpanda93 opened a new pull request, #19624:
URL: https://github.com/apache/hudi/pull/19624

   ### Describe the issue this Pull Request addresses
   
   Closes #14554.
   
   The issue asks for `ReflectionUtils#getTopLevelClassesInClasspath` to be 
simplified. Reading it
   closely, the method is also broken for the only way it is ever invoked.
   
   Every classpath entry for the package is turned into a `File`:
   
   ```java
   directories.add(new File(resource.toURI()));
   ```
   
   A package inside a jar arrives as a `jar:` URL, for example
   `jar:file:/path/to/bundle.jar!/org/apache/hudi/hive/bundle`. That URI is not 
hierarchical, so `File`
   rejects it. Every caller of this method is a bundle `Main` class, which runs 
from inside a shaded
   jar, so all fourteen of them fail:
   
   ```
   $ java -cp 
packaging/hudi-hive-sync-bundle/target/hudi-hive-sync-bundle-1.3.0-SNAPSHOT.jar 
\
          org.apache.hudi.hive.bundle.Main
   Exception in thread "main" java.lang.IllegalArgumentException: URI is not 
hierarchical
        at java.base/java.io.File.<init>(File.java:420)
        at 
org.apache.hudi.common.util.ReflectionUtils.getTopLevelClassesInClasspath(ReflectionUtils.java:145)
        at org.apache.hudi.hive.bundle.Main.main(Main.java:34)
   ```
   
   Those `Main` classes exist to give the packaging modules a source file, as 
their javadoc says, so
   nothing runs them and the breakage has gone unnoticed. It is a total failure 
rather than a latent
   one, though, and it is the behaviour anyone gets who runs one.
   
   Two further paths through the method throw rather than report nothing found:
   
   - `getResources` throwing `IOException` is logged, and then 
`Objects.requireNonNull(resources)`
     turns it into an `NPE` on the next line. The catch makes the failure worse 
than no catch at all.
   - `clazz.getPackage()` is null for arrays and primitives, and is 
dereferenced immediately.
   
   ### Summary and Changelog
   
   - Read jar entries through `JarURLConnection` rather than converting the URL 
to a `File`. The
     directory branch is kept for exploded classpaths, so both layouts are 
supported.
   - `getResources` failing now yields an empty stream instead of an `NPE`.
   - A class with no package now yields an empty stream instead of an `NPE`.
   - The recursive `findClasses` helper is replaced by `Files.walk`, and the 
method body is a flat
     `getResources` to `flatMap` pipeline, which is the simplification the 
issue asked for.
   - Class names are collected before the jar or the directory walk is closed, 
since the returned
     stream outlives the method.
   
   `connection.setUseCaches(false)` is set before the `JarFile` is closed. Jar 
connections are cached
   by default, and closing a cached `JarFile` closes it for every other reader 
of the same jar in the
   process.
   
   Behaviour that is deliberately unchanged: subpackages are still included, as 
the javadoc states, and
   the returned names are still fully qualified class names.
   
   ### Impact
   
   Bundle `Main` classes run instead of throwing. Nothing else calls this 
method, so there is no other
   runtime impact.
   
   Callers who previously received an `NPE` from a class with no package, or 
from a failed
   `getResources`, now receive an empty stream. No caller depended on those 
exceptions.
   
   ### Risk Level
   
   low
   
   One method and its helpers, in a utility with no production callers beyond 
the packaging `Main`
   classes. Covered by five new unit tests, and the end to end result is a 
bundle `Main` that used to
   fail and now succeeds.
   
   ### Documentation Update
   
   None. No configuration or public API change; the method signature and 
documented behaviour are
   unchanged.
   
   ### Verification
   
   **End to end, on the real usage.** The same command against the same bundle, 
differing only in the
   embedded `ReflectionUtils`:
   
   | | Result |
   | --- | --- |
   | Before | exit 1, `IllegalArgumentException: URI is not hierarchical` |
   | After | exit 0, prints `org.apache.hudi.hive.bundle.Main` |
   
   **Unit tests**, added to `TestReflectionUtils`:
   
   | Test | Covers |
   | --- | --- |
   | `testGetTopLevelClassesInClasspathFromDirectory` | an exploded directory, 
built as a fixture, over the `file` protocol |
   | `testGetTopLevelClassesInClasspathFromJar` | a jar, built as a fixture, 
over the `jar` protocol |
   | `testGetTopLevelClassesInClasspathOnTheRealClasspath` | whichever protocol 
the JVM presents |
   | `testGetTopLevelClassesInClasspathForClassesWithoutAPackage` | arrays and 
primitives |
   | `testGetTopLevelClassesInClasspathForPackageNotOnTheClasspath` | no 
resource matches the package |
   
   Both protocol tests build their own classpath root and load it through a 
parent-less
   `URLClassLoader`, then assert the exact expected list: two classes, one 
class in a subpackage, and
   neither a non-class file nor a class outside the package. Building the 
fixtures matters, because
   under Maven the modules are on the test classpath as jars, so a test that 
scans the real classpath
   exercises the jar branch and leaves the directory branch uncovered.
   
   **Negative control.** Against the current implementation, exactly three of 
the new tests fail, for
   the reasons claimed: `FromJar` and `OnTheRealClasspath` with 
`IllegalArgumentException: URI is not
   hierarchical`, and `ForClassesWithoutAPackage` with
   `NullPointerException: Cannot invoke "java.lang.Package.getName()"`. 
`FromDirectory` and
   `ForPackageNotOnTheClasspath` pass either way, since those paths already 
worked.
   
   Full `hudi-io` suite: 125 tests, no failures. `checkstyle` and `apache-rat` 
clean on `hudi-io` and
   `hudi-common`.
   
   ### Related work
   
   #17675 is an open PR against this issue by @VahidRamezaniDB, who found the 
jar problem first, after
   @yihua asked for a unit test on it. This PR reaches the same conclusion 
independently and differs in
   two ways: jar entries are read through `JarURLConnection` rather than by 
decoding and splitting the
   URL string, and the tests build directory and jar fixtures so each protocol 
is actually exercised
   rather than asserting that a scan of the real classpath returns a non-zero 
count.
   
   Happy to close this in favour of that one if a committer prefers; the 
reproduction and the before and
   after above apply to either.
   
   ### Contributor's checklist
   
   - [x] Read through [contributor's 
guide](https://hudi.apache.org/contribute/how-to-contribute)
   - [x] Change Logs and Impact were stated clearly
   - [x] Adequate tests were added if applicable
   - [x] CI passed
   


-- 
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