Discussion:
Add posibility to add custom ModuleReaderFactory to ModuleFinder
(too old to reply)
Alex Sviridov
2018-09-28 09:01:51 UTC
Permalink
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
Alan Bateman
2018-09-28 12:03:46 UTC
Permalink
Post by 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.
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
b
Alex Sviridov
2018-09-28 12:10:59 UTC
Permalink
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
Post by Alan Bateman
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
         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
Alan Bateman
2018-09-28 13:51:56 UTC
Permalink
Post by 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?
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
2018-09-28 14:30:27 UTC
Permalink
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
Post by Alan Bateman
Post by 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?
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
         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()) {
                 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);
         }
         public Optional<ModuleReference> find(String name) {
             if (name.equals(mref.descriptor().name())) {
                 return Optional.of(mref);
             } else {
                 return Optional.empty();
             }
         }
         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
Alan Bateman
2018-09-28 14:54:36 UTC
Permalink
Post by 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);
Yes, this is way to compose them.
Post by Alex Sviridov
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
Remi Forax
2018-09-28 16:02:13 UTC
Permalink
----- Mail original -----
Envoyé: Vendredi 28 Septembre 2018 15:51:56
Objet: Re: Add posibility to add custom ModuleReaderFactory to ModuleFinder
Post by 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?
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
        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()) {
                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);
        }
        public Optional<ModuleReference> find(String name) {
            if (name.equals(mref.descriptor().name())) {
                return Optional.of(mref);
            } else {
                return Optional.empty();
            }
        }
        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

Loading...