|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
package jdk.internal.module; |
|
|
|
import java.io.Closeable; |
|
import java.io.File; |
|
import java.io.IOError; |
|
import java.io.IOException; |
|
import java.io.InputStream; |
|
import java.io.UncheckedIOException; |
|
import java.lang.module.ModuleDescriptor; |
|
import java.lang.module.ModuleDescriptor.Builder; |
|
import java.lang.module.ModuleReader; |
|
import java.lang.module.ModuleReference; |
|
import java.net.MalformedURLException; |
|
import java.net.URI; |
|
import java.net.URL; |
|
import java.nio.ByteBuffer; |
|
import java.nio.file.Files; |
|
import java.nio.file.Path; |
|
import java.nio.file.Paths; |
|
import java.util.ArrayList; |
|
import java.util.Collections; |
|
import java.util.HashMap; |
|
import java.util.HashSet; |
|
import java.util.List; |
|
import java.util.Map; |
|
import java.util.Optional; |
|
import java.util.Set; |
|
import java.util.jar.JarEntry; |
|
import java.util.jar.JarFile; |
|
import java.util.stream.Collectors; |
|
import java.util.stream.Stream; |
|
|
|
import jdk.internal.loader.Resource; |
|
import jdk.internal.misc.JavaLangModuleAccess; |
|
import jdk.internal.misc.SharedSecrets; |
|
import sun.net.www.ParseUtil; |
|
|
|
|
|
/** |
|
* Provides support for patching modules, mostly the boot layer. |
|
*/ |
|
|
|
public final class ModulePatcher { |
|
|
|
private static final JavaLangModuleAccess JLMA |
|
= SharedSecrets.getJavaLangModuleAccess(); |
|
|
|
|
|
private final Map<String, List<Path>> map; |
|
|
|
|
|
|
|
|
|
*/ |
|
public ModulePatcher(Map<String, List<String>> input) { |
|
if (input.isEmpty()) { |
|
this.map = Collections.emptyMap(); |
|
} else { |
|
Map<String, List<Path>> map = new HashMap<>(); |
|
for (Map.Entry<String, List<String>> e : input.entrySet()) { |
|
String mn = e.getKey(); |
|
List<Path> paths = e.getValue().stream() |
|
.map(Paths::get) |
|
.collect(Collectors.toList()); |
|
map.put(mn, paths); |
|
} |
|
this.map = map; |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
public ModuleReference patchIfNeeded(ModuleReference mref) { |
|
|
|
ModuleDescriptor descriptor = mref.descriptor(); |
|
String mn = descriptor.name(); |
|
List<Path> paths = map.get(mn); |
|
if (paths == null) |
|
return mref; |
|
|
|
// Scan the JAR file or directory tree to get the set of packages. |
|
// For automatic modules then packages that do not contain class files |
|
|
|
Set<String> packages = new HashSet<>(); |
|
boolean isAutomatic = descriptor.isAutomatic(); |
|
try { |
|
for (Path file : paths) { |
|
if (Files.isRegularFile(file)) { |
|
|
|
// JAR file - do not open as a multi-release JAR as this |
|
|
|
try (JarFile jf = new JarFile(file.toString())) { |
|
jf.stream() |
|
.filter(e -> !e.isDirectory() |
|
&& (!isAutomatic || e.getName().endsWith(".class"))) |
|
.map(e -> toPackageName(file, e)) |
|
.filter(Checks::isPackageName) |
|
.forEach(packages::add); |
|
} |
|
|
|
} else if (Files.isDirectory(file)) { |
|
|
|
|
|
Path top = file; |
|
Files.find(top, Integer.MAX_VALUE, |
|
((path, attrs) -> attrs.isRegularFile())) |
|
.filter(path -> (!isAutomatic |
|
|| path.toString().endsWith(".class")) |
|
&& !isHidden(path)) |
|
.map(path -> toPackageName(top, path)) |
|
.filter(Checks::isPackageName) |
|
.forEach(packages::add); |
|
|
|
} |
|
} |
|
|
|
} catch (IOException ioe) { |
|
throw new UncheckedIOException(ioe); |
|
} |
|
|
|
|
|
packages.removeAll(descriptor.packages()); |
|
if (!packages.isEmpty()) { |
|
Builder builder = JLMA.newModuleBuilder(descriptor.name(), |
|
false, |
|
descriptor.modifiers()); |
|
if (!descriptor.isAutomatic()) { |
|
descriptor.requires().forEach(builder::requires); |
|
descriptor.exports().forEach(builder::exports); |
|
descriptor.opens().forEach(builder::opens); |
|
descriptor.uses().forEach(builder::uses); |
|
} |
|
descriptor.provides().forEach(builder::provides); |
|
|
|
descriptor.version().ifPresent(builder::version); |
|
descriptor.mainClass().ifPresent(builder::mainClass); |
|
|
|
|
|
builder.packages(descriptor.packages()); |
|
builder.packages(packages); |
|
|
|
descriptor = builder.build(); |
|
} |
|
|
|
|
|
URI location = mref.location().orElse(null); |
|
|
|
ModuleTarget target = null; |
|
ModuleHashes recordedHashes = null; |
|
ModuleHashes.HashSupplier hasher = null; |
|
ModuleResolution mres = null; |
|
if (mref instanceof ModuleReferenceImpl) { |
|
ModuleReferenceImpl impl = (ModuleReferenceImpl)mref; |
|
target = impl.moduleTarget(); |
|
recordedHashes = impl.recordedHashes(); |
|
hasher = impl.hasher(); |
|
mres = impl.moduleResolution(); |
|
} |
|
|
|
return new ModuleReferenceImpl(descriptor, |
|
location, |
|
() -> new PatchedModuleReader(paths, mref), |
|
this, |
|
target, |
|
recordedHashes, |
|
hasher, |
|
mres); |
|
|
|
} |
|
|
|
|
|
|
|
*/ |
|
public boolean hasPatches() { |
|
return !map.isEmpty(); |
|
} |
|
|
|
|
|
|
|
*/ |
|
Set<String> patchedModules() { |
|
return map.keySet(); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
public static class PatchedModuleReader implements ModuleReader { |
|
private final List<ResourceFinder> finders; |
|
private final ModuleReference mref; |
|
private final URL delegateCodeSourceURL; |
|
private volatile ModuleReader delegate; |
|
|
|
|
|
|
|
*/ |
|
PatchedModuleReader(List<Path> patches, ModuleReference mref) { |
|
List<ResourceFinder> finders = new ArrayList<>(); |
|
boolean initialized = false; |
|
try { |
|
for (Path file : patches) { |
|
if (Files.isRegularFile(file)) { |
|
finders.add(new JarResourceFinder(file)); |
|
} else { |
|
finders.add(new ExplodedResourceFinder(file)); |
|
} |
|
} |
|
initialized = true; |
|
} catch (IOException ioe) { |
|
throw new UncheckedIOException(ioe); |
|
} finally { |
|
|
|
if (!initialized) closeAll(finders); |
|
} |
|
|
|
this.finders = finders; |
|
this.mref = mref; |
|
this.delegateCodeSourceURL = codeSourceURL(mref); |
|
} |
|
|
|
|
|
|
|
*/ |
|
private static void closeAll(List<ResourceFinder> finders) { |
|
for (ResourceFinder finder : finders) { |
|
try { finder.close(); } catch (IOException ioe) { } |
|
} |
|
} |
|
|
|
|
|
|
|
*/ |
|
private static URL codeSourceURL(ModuleReference mref) { |
|
try { |
|
Optional<URI> ouri = mref.location(); |
|
if (ouri.isPresent()) |
|
return ouri.get().toURL(); |
|
} catch (MalformedURLException e) { } |
|
return null; |
|
} |
|
|
|
|
|
|
|
|
|
*/ |
|
private ModuleReader delegate() throws IOException { |
|
ModuleReader r = delegate; |
|
if (r == null) { |
|
synchronized (this) { |
|
r = delegate; |
|
if (r == null) { |
|
delegate = r = mref.open(); |
|
} |
|
} |
|
} |
|
return r; |
|
} |
|
|
|
|
|
|
|
|
|
*/ |
|
private Resource findResourceInPatch(String name) throws IOException { |
|
if (!name.equals("module-info.class")) { |
|
for (ResourceFinder finder : finders) { |
|
Resource r = finder.find(name); |
|
if (r != null) |
|
return r; |
|
} |
|
} |
|
return null; |
|
} |
|
|
|
|
|
|
|
*/ |
|
public Resource findResource(String name) throws IOException { |
|
|
|
|
|
Resource r = findResourceInPatch(name); |
|
if (r != null) |
|
return r; |
|
|
|
|
|
ByteBuffer bb = delegate().read(name).orElse(null); |
|
if (bb == null) |
|
return null; |
|
|
|
return new Resource() { |
|
private <T> T shouldNotGetHere(Class<T> type) { |
|
throw new InternalError("should not get here"); |
|
} |
|
@Override |
|
public String getName() { |
|
return shouldNotGetHere(String.class); |
|
} |
|
@Override |
|
public URL getURL() { |
|
return shouldNotGetHere(URL.class); |
|
} |
|
@Override |
|
public URL getCodeSourceURL() { |
|
return delegateCodeSourceURL; |
|
} |
|
@Override |
|
public ByteBuffer getByteBuffer() throws IOException { |
|
return bb; |
|
} |
|
@Override |
|
public InputStream getInputStream() throws IOException { |
|
return shouldNotGetHere(InputStream.class); |
|
} |
|
@Override |
|
public int getContentLength() throws IOException { |
|
return shouldNotGetHere(int.class); |
|
} |
|
}; |
|
} |
|
|
|
@Override |
|
public Optional<URI> find(String name) throws IOException { |
|
Resource r = findResourceInPatch(name); |
|
if (r != null) { |
|
URI uri = URI.create(r.getURL().toString()); |
|
return Optional.of(uri); |
|
} else { |
|
return delegate().find(name); |
|
} |
|
} |
|
|
|
@Override |
|
public Optional<InputStream> open(String name) throws IOException { |
|
Resource r = findResourceInPatch(name); |
|
if (r != null) { |
|
return Optional.of(r.getInputStream()); |
|
} else { |
|
return delegate().open(name); |
|
} |
|
} |
|
|
|
@Override |
|
public Optional<ByteBuffer> read(String name) throws IOException { |
|
Resource r = findResourceInPatch(name); |
|
if (r != null) { |
|
ByteBuffer bb = r.getByteBuffer(); |
|
assert !bb.isDirect(); |
|
return Optional.of(bb); |
|
} else { |
|
return delegate().read(name); |
|
} |
|
} |
|
|
|
@Override |
|
public void release(ByteBuffer bb) { |
|
if (bb.isDirect()) { |
|
try { |
|
delegate().release(bb); |
|
} catch (IOException ioe) { |
|
throw new InternalError(ioe); |
|
} |
|
} |
|
} |
|
|
|
@Override |
|
public Stream<String> list() throws IOException { |
|
Stream<String> s = delegate().list(); |
|
for (ResourceFinder finder : finders) { |
|
s = Stream.concat(s, finder.list()); |
|
} |
|
return s.distinct(); |
|
} |
|
|
|
@Override |
|
public void close() throws IOException { |
|
closeAll(finders); |
|
delegate().close(); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
*/ |
|
private static interface ResourceFinder extends Closeable { |
|
Resource find(String name) throws IOException; |
|
Stream<String> list() throws IOException; |
|
} |
|
|
|
|
|
|
|
|
|
*/ |
|
private static class JarResourceFinder implements ResourceFinder { |
|
private final JarFile jf; |
|
private final URL csURL; |
|
|
|
JarResourceFinder(Path path) throws IOException { |
|
this.jf = new JarFile(path.toString()); |
|
this.csURL = path.toUri().toURL(); |
|
} |
|
|
|
@Override |
|
public void close() throws IOException { |
|
jf.close(); |
|
} |
|
|
|
@Override |
|
public Resource find(String name) throws IOException { |
|
JarEntry entry = jf.getJarEntry(name); |
|
if (entry == null) |
|
return null; |
|
|
|
return new Resource() { |
|
@Override |
|
public String getName() { |
|
return name; |
|
} |
|
@Override |
|
public URL getURL() { |
|
String encodedPath = ParseUtil.encodePath(name, false); |
|
try { |
|
return new URL("jar:" + csURL + "!/" + encodedPath); |
|
} catch (MalformedURLException e) { |
|
return null; |
|
} |
|
} |
|
@Override |
|
public URL getCodeSourceURL() { |
|
return csURL; |
|
} |
|
@Override |
|
public ByteBuffer getByteBuffer() throws IOException { |
|
byte[] bytes = getInputStream().readAllBytes(); |
|
return ByteBuffer.wrap(bytes); |
|
} |
|
@Override |
|
public InputStream getInputStream() throws IOException { |
|
return jf.getInputStream(entry); |
|
} |
|
@Override |
|
public int getContentLength() throws IOException { |
|
long size = entry.getSize(); |
|
return (size > Integer.MAX_VALUE) ? -1 : (int) size; |
|
} |
|
}; |
|
} |
|
|
|
@Override |
|
public Stream<String> list() throws IOException { |
|
return jf.stream().map(JarEntry::getName); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
*/ |
|
private static class ExplodedResourceFinder implements ResourceFinder { |
|
private final Path dir; |
|
|
|
ExplodedResourceFinder(Path dir) { |
|
this.dir = dir; |
|
} |
|
|
|
@Override |
|
public void close() { } |
|
|
|
@Override |
|
public Resource find(String name) throws IOException { |
|
Path file = Resources.toFilePath(dir, name); |
|
if (file != null) { |
|
return newResource(name, dir, file); |
|
} else { |
|
return null; |
|
} |
|
} |
|
|
|
private Resource newResource(String name, Path top, Path file) { |
|
return new Resource() { |
|
@Override |
|
public String getName() { |
|
return name; |
|
} |
|
@Override |
|
public URL getURL() { |
|
try { |
|
return file.toUri().toURL(); |
|
} catch (IOException | IOError e) { |
|
return null; |
|
} |
|
} |
|
@Override |
|
public URL getCodeSourceURL() { |
|
try { |
|
return top.toUri().toURL(); |
|
} catch (IOException | IOError e) { |
|
return null; |
|
} |
|
} |
|
@Override |
|
public ByteBuffer getByteBuffer() throws IOException { |
|
return ByteBuffer.wrap(Files.readAllBytes(file)); |
|
} |
|
@Override |
|
public InputStream getInputStream() throws IOException { |
|
return Files.newInputStream(file); |
|
} |
|
@Override |
|
public int getContentLength() throws IOException { |
|
long size = Files.size(file); |
|
return (size > Integer.MAX_VALUE) ? -1 : (int)size; |
|
} |
|
}; |
|
} |
|
|
|
@Override |
|
public Stream<String> list() throws IOException { |
|
return Files.walk(dir, Integer.MAX_VALUE) |
|
.map(f -> Resources.toResourceName(dir, f)) |
|
.filter(s -> s.length() > 0); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
*/ |
|
private static String toPackageName(Path top, Path file) { |
|
Path entry = top.relativize(file); |
|
Path parent = entry.getParent(); |
|
if (parent == null) { |
|
return warnIfModuleInfo(top, entry.toString()); |
|
} else { |
|
return parent.toString().replace(File.separatorChar, '.'); |
|
} |
|
} |
|
|
|
|
|
|
|
*/ |
|
private boolean isHidden(Path file) { |
|
try { |
|
return Files.isHidden(file); |
|
} catch (IOException ioe) { |
|
return false; |
|
} |
|
} |
|
|
|
|
|
|
|
*/ |
|
private static String toPackageName(Path file, JarEntry entry) { |
|
String name = entry.getName(); |
|
int index = name.lastIndexOf("/"); |
|
if (index == -1) { |
|
return warnIfModuleInfo(file, name); |
|
} else { |
|
return name.substring(0, index).replace('/', '.'); |
|
} |
|
} |
|
|
|
private static String warnIfModuleInfo(Path file, String e) { |
|
if (e.equals("module-info.class")) |
|
System.err.println("WARNING: " + e + " ignored in patch: " + file); |
|
return ""; |
|
} |
|
} |