Add posibility to add custom ModuleReaderFactory to ModuleFinder

classic Classic list List threaded Threaded
7 messages Options
Reply | Threaded
Open this post in threaded view
|

Add posibility to add custom ModuleReaderFactory to ModuleFinder

Alex Sviridov
From java 9 we can create JPMS layers and dynamically add modules to it. At the same time different types of java archives can be JPMS modules - .jar, .war, .ear. However, the problem is that it is possible to add only .jar archives by default. I opened an issue here  https://bugs.openjdk.java.net/browse/JDK-8203330  and as it was found out it is necessary to implement custom ModuleFinder. 

Trying to implement ModuleFinder I understood that it is necessary to rewrite (or take from) the "half" of jdk.intenal what is a very bad way. 

Lets' consider what I need to add .war arhives to JPMS layer. I need: 1) to understand if I can work with this type of archive or I can not (if .war is supported) 2). to map file location, for example instead of "module-info.java" I must find "WEB-INF/classes/module-info.java" etc. For that I don't need to create ModuleDescriptor, ModuleReference, ModuleFinder. 

So I suggest to overload ModuleFinder.of() method adding as a parameter custom ModuleReaderFactory. For example to add ModuleFinder#of(ModuleReaderFactory factory, Path... entries). ModuleReaderFactory must have such method - createModuleReader(Path path). Such solution will also help Jakarta EE developers a lot of. 


Best regards, Pavel

Reply | Threaded
Open this post in threaded view
|

Re: Add posibility to add custom ModuleReaderFactory to ModuleFinder

Alan Bateman
On 28/09/2018 10:01, Alex Sviridov wrote:
>  From java 9 we can create JPMS layers and dynamically add modules to it. At the same time different types of java archives can be JPMS modules - .jar, .war, .ear. However, the problem is that it is possible to add only .jar archives by default. I opened an issue here  https://bugs.openjdk.java.net/browse/JDK-8203330  and as it was found out it is necessary to implement custom ModuleFinder.
>
> Trying to implement ModuleFinder I understood that it is necessary to rewrite (or take from) the "half" of jdk.intenal what is a very bad way.
>
> Lets' consider what I need to add .war arhives to JPMS layer. I need: 1) to understand if I can work with this type of archive or I can not (if .war is supported) 2). to map file location, for example instead of "module-info.java" I must find "WEB-INF/classes/module-info.java" etc. For that I don't need to create ModuleDescriptor, ModuleReference, ModuleFinder.
>
> So I suggest to overload ModuleFinder.of() method adding as a parameter custom ModuleReaderFactory. For example to add ModuleFinder#of(ModuleReaderFactory factory, Path... entries). ModuleReaderFactory must have such method - createModuleReader(Path path). Such solution will also help Jakarta EE developers a lot of.
>
Creating a ModuleFinder that can find modules in WAR files shouldn't
need any API additions or copying of code from the JDK. Where are the
pain points that you are running into? Is it because a WAR files is
single artifact that may contain several modules (the application module
under WEB-INF/classes and dependences are packaged as JAR files under
WEB-INF/lib). One thing to be aware of is that the zip file system
provider was updated recently to improve its support for opening JAR
files in custom file systems, this means you can do things like this:

         ClassLoader scl = ClassLoader.getSystemClassLoader();
         try (FileSystem warfs = FileSystems.newFileSystem(war, scl)) {
             Path classes = warfs.getPath("/WEB-INF/classes");
             Files.walk(classes)
                     .map(p -> classes.relativize(p))
                     .forEach(System.out::println);

             Path lib = warfs.getPath("WEB-INF/lib");
             Files.find(lib, 1, (path, attrs) ->
path.toString().endsWith("jar"))
                     .forEach(jar -> {
                         try (FileSystem jarfs =
FileSystems.newFileSystem(jar, scl)) {
                             Path top = jarfs.getPath("/");
                             Files.walk(top)
                                     .map(p -> top.relativize(p))
                                     .forEach(System.out::println);
                         } catch (IOException ioe) {
                             throw new UncheckedIOException(ioe);
                         }
                     });
         }

Another thing to be aware of is that the ModuleFinder.of(Path[]) can
also deal with JAR files that are packaged inside other JAR files. It
does have to extract them to a temporary location on the file system and
there may several potential improvements that could be just, just hasn't
been an area to spend time on.

-Alan.



Reply | Threaded
Open this post in threaded view
|

Re[2]: Add posibility to add custom ModuleReaderFactory to ModuleFinder

Alex Sviridov
Hi Alan

Thank you for your answer. But my main problem is not jars inside .war - this is a so far from my current problem. Now I need to 1) add .war file to layer 2). to map file location, for example instead of "module-info.java" I must find "WEB-INF/classes/module-info.java" etc. That is all I need. How can I do it without implementing ModuleFinder?

Best regards, Pavel


>Пятница, 28 сентября 2018, 15:03 +03:00 от Alan Bateman <[hidden email]>:
>
>On 28/09/2018 10:01, Alex Sviridov wrote:
>>  From java 9 we can create JPMS layers and dynamically add modules to it. At the same time different types of java archives can be JPMS modules - .jar, .war, .ear. However, the problem is that it is possible to add only .jar archives by default. I opened an issue here   https://bugs.openjdk.java.net/browse/JDK-8203330  and as it was found out it is necessary to implement custom ModuleFinder.
>>
>> Trying to implement ModuleFinder I understood that it is necessary to rewrite (or take from) the "half" of jdk.intenal what is a very bad way.
>>
>> Lets' consider what I need to add .war arhives to JPMS layer. I need: 1) to understand if I can work with this type of archive or I can not (if .war is supported) 2). to map file location, for example instead of "module-info.java" I must find "WEB-INF/classes/module-info.java" etc. For that I don't need to create ModuleDescriptor, ModuleReference, ModuleFinder.
>>
>> So I suggest to overload ModuleFinder.of() method adding as a parameter custom ModuleReaderFactory. For example to add ModuleFinder#of(ModuleReaderFactory factory, Path... entries). ModuleReaderFactory must have such method - createModuleReader(Path path). Such solution will also help Jakarta EE developers a lot of.
>>
>Creating a ModuleFinder that can find modules in WAR files shouldn't
>need any API additions or copying of code from the JDK. Where are the
>pain points that you are running into? Is it because a WAR files is
>single artifact that may contain several modules (the application module
>under WEB-INF/classes and dependences are packaged as JAR files under
>WEB-INF/lib). One thing to be aware of is that the zip file system
>provider was updated recently to improve its support for opening JAR
>files in custom file systems, this means you can do things like this:
>
>         ClassLoader scl = ClassLoader.getSystemClassLoader();
>         try (FileSystem warfs = FileSystems.newFileSystem(war, scl)) {
>             Path classes = warfs.getPath("/WEB-INF/classes");
>             Files.walk(classes)
>                     .map(p -> classes.relativize(p))
>                     .forEach(System.out::println);
>
>             Path lib = warfs.getPath("WEB-INF/lib");
>             Files.find(lib, 1, (path, attrs) ->
>path.toString().endsWith("jar"))
>                     .forEach(jar -> {
>                         try (FileSystem jarfs =
>FileSystems.newFileSystem(jar, scl)) {
>                             Path top = jarfs.getPath("/");
>                             Files.walk(top)
>                                     .map(p -> top.relativize(p))
>                                     .forEach(System.out::println);
>                         } catch (IOException ioe) {
>                             throw new UncheckedIOException(ioe);
>                         }
>                     });
>         }
>
>Another thing to be aware of is that the ModuleFinder.of(Path[]) can
>also deal with JAR files that are packaged inside other JAR files. It
>does have to extract them to a temporary location on the file system and
>there may several potential improvements that could be just, just hasn't
>been an area to spend time on.
>
>-Alan.
>
>
>


--
Alex Sviridov
Reply | Threaded
Open this post in threaded view
|

Re: Add posibility to add custom ModuleReaderFactory to ModuleFinder

Alan Bateman
On 28/09/2018 13:10, Alex Sviridov wrote:
> Hi Alan
>
> Thank you for your answer. But my main problem is not jars inside .war
> - this is a so far from my current problem. Now I need to 1) add .war
> file to layer 2). to map file location, for example instead of
> "module-info.java" I must find "WEB-INF/classes/module-info.java" etc.
> That is all I need. How can I do it without implementing ModuleFinder?
You'll need a ModuleFinder because the packaging formats that
ModuleFinder.of(Path) is required to support doesn't know anything about
WAR files. It's not super difficult to develop your own. I attach a
simple implementation that may get you started. It's really basic but
would need a few iterations to be robust. Invoke
WarModuleFinder.of(Path) with the file path to the WAR file and it will
create a ModuleFinder that can find the application module in the WAR
file. A more complete implementation would be a lot more robust and
polished that this sample, it would also find the modules WEB-INF/lib.

Once you have a ModuleFinder then you specify it to Conguration::resolve
method when resolving the application as the root module. You'll
probably start with something like:

         Path war = Path.of("app.war");
         ModuleFinder finder = WarModuleFinder.of(war);

         String appModuleName = finder.findAll().stream()
                 .findFirst()
                 .map(ModuleReference::descriptor)
                 .map(ModuleDescriptor::name)
                 .orElseThrow();

         ModuleLayer boot = ModuleLayer.boot();
         Configuration cf = boot.configuration().resolve(finder,
ModuleFinder.of(), Set.of(appModuleName));
         ModuleLayer layer = boot.defineModulesWithOneLoader(cf,
ClassLoader.getSystemClassLoader());

and now you have a module layer with the application module loaded from
the WEB-INF/classes part of the WAR file.

-Alan


     static class WarModuleFinder implements ModuleFinder {
         private final FileSystem warfs;
         private final Path classes;
         private final ModuleReference mref;

         private WarModuleFinder(Path warfile) throws IOException {
             ClassLoader scl = ClassLoader.getSystemClassLoader();
             FileSystem fs = FileSystems.newFileSystem(warfile, scl);
             Path classes = fs.getPath("/WEB-INF/classes");

             ModuleDescriptor descriptor;
             try (InputStream in =
Files.newInputStream(classes.resolve("module-info.class"))) {
                 descriptor = ModuleDescriptor.read(in, () ->
packages(classes));
             }

             this.warfs = fs;
             this.classes = classes;
             this.mref = new ModuleReference(descriptor, classes.toUri()) {
                 @Override
                 public ModuleReader open() {
                     return new WarModuleReader();
                 }
                 public String toString() {
                     StringBuilder sb = new StringBuilder();
                     sb.append("[module ");
                     sb.append(descriptor().name());
                     sb.append(", location=");
                     sb.append(location());
                     sb.append("]");
                     return sb.toString();
                 }
             };
         }

         static WarModuleFinder of(Path war) throws IOException {
             return new WarModuleFinder(war);
         }

         @Override
         public Optional<ModuleReference> find(String name) {
             if (name.equals(mref.descriptor().name())) {
                 return Optional.of(mref);
             } else {
                 return Optional.empty();
             }
         }

         @Override
         public Set<ModuleReference> findAll() {
             return Set.of(mref);
         }

         private Set<String> packages(Path classes) {
             try {
                 return Files.find(classes, Integer.MAX_VALUE,
                                   (path, attrs) -> !attrs.isDirectory())
                         .map(entry -> classes.relativize(entry).toString())
                         .map(this::toPackageName)
                         .flatMap(Optional::stream)
                         .collect(Collectors.toSet());
             } catch (IOException ioe) {
                 throw new UncheckedIOException(ioe);
             }
         }

         private Optional<String> toPackageName(String name) {
             int index = name.lastIndexOf("/");
             if (index > 0) {
                 return Optional.of(name.substring(0,
index).replace('/', '.'));
             } else {
                 return Optional.empty();
             }
         }

         class WarModuleReader implements ModuleReader {
             private volatile boolean closed;

             private void ensureOpen() throws IOException {
                 if (closed) throw new IOException("ModuleReader is
closed");
             }

             public Optional<URI> find(String name) throws IOException {
                 ensureOpen();
                 if (!name.startsWith("/")) {
                     Path entry = classes.resolve(name);
                     if (Files.exists(entry)) {
                         return Optional.of(entry.toUri());
                     }
                 }
                 return Optional.empty();
             }

             public Stream<String> list() throws IOException {
                 ensureOpen();
                 return Files.walk(classes)
                         .map(entry -> classes.relativize(entry).toString())
                         .filter(name -> name.length() > 0);
             }

             public void close() {
                 closed = true;
             }
         }
     }

Reply | Threaded
Open this post in threaded view
|

Re[2]: Add posibility to add custom ModuleReaderFactory to ModuleFinder

Alex Sviridov
Alan, thank you very much for your help. This is what I was looking for.
Only one moment - as I  understand the ModuleFinder that you implemented can
work only with .war modules. However, at one JPMS layer I want to place .war
modules and .jar modules. What should I do in such situation? Should I do
ModuleFinder commonFinder = ModuleFinder.compose(jarFinder, warFinder);
Configuration cf = boot.configuration().resolve(commonFinder,  ModuleFinder.of(), Set.of(jars+wars)); ?

Best regards, Pavel


>Пятница, 28 сентября 2018, 16:52 +03:00 от Alan Bateman <[hidden email]>:
>
>On 28/09/2018 13:10, Alex Sviridov wrote:
>> Hi Alan
>>
>> Thank you for your answer. But my main problem is not jars inside .war
>> - this is a so far from my current problem. Now I need to 1) add .war
>> file to layer 2). to map file location, for example instead of
>> "module-info.java" I must find "WEB-INF/classes/module-info.java" etc.
>> That is all I need. How can I do it without implementing ModuleFinder?
>You'll need a ModuleFinder because the packaging formats that
>ModuleFinder.of(Path) is required to support doesn't know anything about
>WAR files. It's not super difficult to develop your own. I attach a
>simple implementation that may get you started. It's really basic but
>would need a few iterations to be robust. Invoke
>WarModuleFinder.of(Path) with the file path to the WAR file and it will
>create a ModuleFinder that can find the application module in the WAR
>file. A more complete implementation would be a lot more robust and
>polished that this sample, it would also find the modules WEB-INF/lib.
>
>Once you have a ModuleFinder then you specify it to Conguration::resolve
>method when resolving the application as the root module. You'll
>probably start with something like:
>
>         Path war = Path.of("app.war");
>         ModuleFinder finder = WarModuleFinder.of(war);
>
>         String appModuleName = finder.findAll().stream()
>                 .findFirst()
>                 .map(ModuleReference::descriptor)
>                 .map(ModuleDescriptor::name)
>                 .orElseThrow();
>
>         ModuleLayer boot = ModuleLayer.boot();
>         Configuration cf = boot.configuration().resolve(finder,
>ModuleFinder.of(), Set.of(appModuleName));
>         ModuleLayer layer = boot.defineModulesWithOneLoader(cf,
>ClassLoader.getSystemClassLoader());
>
>and now you have a module layer with the application module loaded from
>the WEB-INF/classes part of the WAR file.
>
>-Alan
>
>
>     static class WarModuleFinder implements ModuleFinder {
>         private final FileSystem warfs;
>         private final Path classes;
>         private final ModuleReference mref;
>
>         private WarModuleFinder(Path warfile) throws IOException {
>             ClassLoader scl = ClassLoader.getSystemClassLoader();
>             FileSystem fs = FileSystems.newFileSystem(warfile, scl);
>             Path classes = fs.getPath("/WEB-INF/classes");
>
>             ModuleDescriptor descriptor;
>             try (InputStream in =
>Files.newInputStream(classes.resolve("module-info.class"))) {
>                 descriptor = ModuleDescriptor.read(in, () ->
>packages(classes));
>             }
>
>             this.warfs = fs;
>             this.classes = classes;
>             this.mref = new ModuleReference(descriptor, classes.toUri()) {
>                 @Override
>                 public ModuleReader open() {
>                     return new WarModuleReader();
>                 }
>                 public String toString() {
>                     StringBuilder sb = new StringBuilder();
>                     sb.append("[module ");
>                     sb.append(descriptor().name());
>                     sb.append(", location=");
>                     sb.append(location());
>                     sb.append("]");
>                     return sb.toString();
>                 }
>             };
>         }
>
>         static WarModuleFinder of(Path war) throws IOException {
>             return new WarModuleFinder(war);
>         }
>
>         @Override
>         public Optional<ModuleReference> find(String name) {
>             if (name.equals(mref.descriptor().name())) {
>                 return Optional.of(mref);
>             } else {
>                 return Optional.empty();
>             }
>         }
>
>         @Override
>         public Set<ModuleReference> findAll() {
>             return Set.of(mref);
>         }
>
>         private Set<String> packages(Path classes) {
>             try {
>                 return Files.find(classes, Integer.MAX_VALUE,
>                                   (path, attrs) -> !attrs.isDirectory())
>                         .map(entry -> classes.relativize(entry).toString())
>                         .map(this::toPackageName)
>                         .flatMap(Optional::stream)
>                         .collect(Collectors.toSet());
>             } catch (IOException ioe) {
>                 throw new UncheckedIOException(ioe);
>             }
>         }
>
>         private Optional<String> toPackageName(String name) {
>             int index = name.lastIndexOf("/");
>             if (index > 0) {
>                 return Optional.of(name.substring(0,
>index).replace('/', '.'));
>             } else {
>                 return Optional.empty();
>             }
>         }
>
>         class WarModuleReader implements ModuleReader {
>             private volatile boolean closed;
>
>             private void ensureOpen() throws IOException {
>                 if (closed) throw new IOException("ModuleReader is
>closed");
>             }
>
>             public Optional<URI> find(String name) throws IOException {
>                 ensureOpen();
>                 if (!name.startsWith("/")) {
>                     Path entry = classes.resolve(name);
>                     if (Files.exists(entry)) {
>                         return Optional.of(entry.toUri());
>                     }
>                 }
>                 return Optional.empty();
>             }
>
>             public Stream<String> list() throws IOException {
>                 ensureOpen();
>                 return Files.walk(classes)
>                         .map(entry -> classes.relativize(entry).toString())
>                         .filter(name -> name.length() > 0);
>             }
>
>             public void close() {
>                 closed = true;
>             }
>         }
>     }
>


--
Alex Sviridov
Reply | Threaded
Open this post in threaded view
|

Re: Add posibility to add custom ModuleReaderFactory to ModuleFinder

Alan Bateman
On 28/09/2018 15:30, Alex Sviridov wrote:
> Alan, thank you very much for your help. This is what I was looking for.
> Only one moment - as I  understand the ModuleFinder that you
> implemented can
> work only with .war modules. However, at one JPMS layer I want to
> place .war
> modules and .jar modules. What should I do in such situation? Should I do
> ModuleFinder commonFinder = ModuleFinder.compose(jarFinder, warFinder);
Yes, this is way to compose them.


> Configuration cf = boot.configuration().resolve(commonFinder, 
> ModuleFinder.of(), Set.of(jars+wars)); ?
For the root modules then I assume you just need the name of the
application module, not the names of the modules in the JAR files. You
may also want to use resolveAndBind so that service provider modules are
also resolved.

-Alan
Reply | Threaded
Open this post in threaded view
|

Re: Add posibility to add custom ModuleReaderFactory to ModuleFinder

Remi Forax
In reply to this post by Alan Bateman


----- Mail original -----
> De: "Alan Bateman" <[hidden email]>
> À: "Alex Sviridov" <[hidden email]>, "jigsaw-dev" <[hidden email]>
> Envoyé: Vendredi 28 Septembre 2018 15:51:56
> Objet: Re: Add posibility to add custom ModuleReaderFactory to ModuleFinder

> On 28/09/2018 13:10, Alex Sviridov wrote:
>> Hi Alan
>>
>> Thank you for your answer. But my main problem is not jars inside .war
>> - this is a so far from my current problem. Now I need to 1) add .war
>> file to layer 2). to map file location, for example instead of
>> "module-info.java" I must find "WEB-INF/classes/module-info.java" etc.
>> That is all I need. How can I do it without implementing ModuleFinder?
> You'll need a ModuleFinder because the packaging formats that
> ModuleFinder.of(Path) is required to support doesn't know anything about
> WAR files. It's not super difficult to develop your own. I attach a
> simple implementation that may get you started. It's really basic but
> would need a few iterations to be robust. Invoke
> WarModuleFinder.of(Path) with the file path to the WAR file and it will
> create a ModuleFinder that can find the application module in the WAR
> file. A more complete implementation would be a lot more robust and
> polished that this sample, it would also find the modules WEB-INF/lib.
>
> Once you have a ModuleFinder then you specify it to Conguration::resolve
> method when resolving the application as the root module. You'll
> probably start with something like:
>
>         Path war = Path.of("app.war");
>         ModuleFinder finder = WarModuleFinder.of(war);
>
>         String appModuleName = finder.findAll().stream()
>                 .findFirst()
>                 .map(ModuleReference::descriptor)
>                 .map(ModuleDescriptor::name)
>                 .orElseThrow();
>
>         ModuleLayer boot = ModuleLayer.boot();
>         Configuration cf = boot.configuration().resolve(finder,
> ModuleFinder.of(), Set.of(appModuleName));
>         ModuleLayer layer = boot.defineModulesWithOneLoader(cf,
> ClassLoader.getSystemClassLoader());
>
> and now you have a module layer with the application module loaded from
> the WEB-INF/classes part of the WAR file.
>
> -Alan
>
>
>     static class WarModuleFinder implements ModuleFinder {
>         private final FileSystem warfs;
>         private final Path classes;
>         private final ModuleReference mref;
>
>         private WarModuleFinder(Path warfile) throws IOException {
>             ClassLoader scl = ClassLoader.getSystemClassLoader();
>             FileSystem fs = FileSystems.newFileSystem(warfile, scl);
>             Path classes = fs.getPath("/WEB-INF/classes");
>
>             ModuleDescriptor descriptor;
>             try (InputStream in =
> Files.newInputStream(classes.resolve("module-info.class"))) {
>                 descriptor = ModuleDescriptor.read(in, () ->
> packages(classes));
>             }
>
>             this.warfs = fs;
>             this.classes = classes;
>             this.mref = new ModuleReference(descriptor, classes.toUri()) {
>                 @Override
>                 public ModuleReader open() {
>                     return new WarModuleReader();
>                 }
>                 public String toString() {
>                     StringBuilder sb = new StringBuilder();
>                     sb.append("[module ");
>                     sb.append(descriptor().name());
>                     sb.append(", location=");
>                     sb.append(location());
>                     sb.append("]");
>                     return sb.toString();
>                 }
>             };
>         }
>
>         static WarModuleFinder of(Path war) throws IOException {
>             return new WarModuleFinder(war);
>         }
>
>         @Override
>         public Optional<ModuleReference> find(String name) {
>             if (name.equals(mref.descriptor().name())) {
>                 return Optional.of(mref);
>             } else {
>                 return Optional.empty();
>             }
>         }
>
>         @Override
>         public Set<ModuleReference> findAll() {
>             return Set.of(mref);
>         }
>
>         private Set<String> packages(Path classes) {
>             try {
>                 return Files.find(classes, Integer.MAX_VALUE,
>                                   (path, attrs) -> !attrs.isDirectory())
>                         .map(entry -> classes.relativize(entry).toString())
>                         .map(this::toPackageName)
>                         .flatMap(Optional::stream)
>                         .collect(Collectors.toSet());
>             } catch (IOException ioe) {
>                 throw new UncheckedIOException(ioe);
>             }
>         }
>
>         private Optional<String> toPackageName(String name) {
>             int index = name.lastIndexOf("/");
>             if (index > 0) {
>                 return Optional.of(name.substring(0,
> index).replace('/', '.'));
>             } else {
>                 return Optional.empty();
>             }
>         }
>
>         class WarModuleReader implements ModuleReader {
>             private volatile boolean closed;
>
>             private void ensureOpen() throws IOException {
>                 if (closed) throw new IOException("ModuleReader is
> closed");
>             }
>
>             public Optional<URI> find(String name) throws IOException {
>                 ensureOpen();
>                 if (!name.startsWith("/")) {
>                     Path entry = classes.resolve(name);
>                     if (Files.exists(entry)) {
>                         return Optional.of(entry.toUri());
>                     }
>                 }
>                 return Optional.empty();
>             }
>
>             public Stream<String> list() throws IOException {
>                 ensureOpen();
>                 return Files.walk(classes)
>                         .map(entry -> classes.relativize(entry).toString())
>                         .filter(name -> name.length() > 0);
>             }
>
>             public void close() {
>                 closed = true;
>             }
>         }
>      }


which give you the following code, if you
- move the code from the constructor to the static factory
- untangle the WarModuleFinder and the WarModuleReader
- use Optional the monadic way (why there is no filter on OptionalInt BTW ?)
- sprinkle some vars on it

import static java.util.function.Predicate.not;

import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.lang.module.ModuleDescriptor;
import java.lang.module.ModuleFinder;
import java.lang.module.ModuleReader;
import java.lang.module.ModuleReference;
import java.net.URI;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class WarModuleFinder implements ModuleFinder {
  private final ModuleReference mref;

  private WarModuleFinder(ModuleReference mref) {
    this.mref = mref;
  }

  public static WarModuleFinder of(Path war) throws IOException {
    var systemClassLoader = ClassLoader.getSystemClassLoader();
    var fileSystem = FileSystems.newFileSystem(war, systemClassLoader);
    var classes = fileSystem.getPath("/WEB-INF/classes");
   
    ModuleDescriptor descriptor;
    try (InputStream in = Files.newInputStream(classes.resolve("module-info.class"))) {
      descriptor = ModuleDescriptor.read(in, () -> packages(classes));
    }  
    return new WarModuleFinder(new ModuleReference(descriptor, classes.toUri()) {
      @Override
      public ModuleReader open() {
        return new WarModuleReader(classes);
      }

      @Override
      public String toString() {
        return "[module " + descriptor().name() + ", location=" + location() + ']';
      }
    });
  }

  @Override
  public Optional<ModuleReference> find(String name) {
    return Optional.of(mref).filter(mref -> name.equals(mref.descriptor().name()));
  }

  @Override
  public Set<ModuleReference> findAll() {
    return Set.of(mref);
  }

  private static Set<String> packages(Path classes) {
    try {
      return Files.find(classes, Integer.MAX_VALUE, (path, attrs) -> !attrs.isDirectory())
          .map(entry -> toPackageName(classes.relativize(entry).toString()))
          .flatMap(Optional::stream)
          .collect(Collectors.toSet());
    } catch (IOException ioe) {
      throw new UncheckedIOException(ioe);
    }
  }

  private static Optional<String> toPackageName(String name) {
    return Optional.of(name.lastIndexOf("/"))
        .filter(index -> index > 0)
        .map(index -> name.substring(0, index).replace('/', '.'));
  }

  private static class WarModuleReader implements ModuleReader {
    private final Path classes;
    private volatile boolean closed;

    private WarModuleReader(Path classes) {
      this.classes = classes;
    }
   
    private void ensureOpen() throws IOException {
      if (closed) {
        throw new IOException("ModuleReader is closed");
      }
    }

    @Override
    public Optional<URI> find(String name) throws IOException {
      ensureOpen();
      return Optional.of(name)
          .filter(not(_name -> _name.startsWith("/")))
          .map(classes::resolve)
          .filter(Files::exists)
          .map(Path::toUri);
    }

    @Override
    public Stream<String> list() throws IOException {
      ensureOpen();
      return Files.walk(classes).map(entry -> classes.relativize(entry).toString()).filter(not(String::isEmpty));
    }

    @Override
    public void close() {
      closed = true;
    }
  }
}

Rémi