All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.eclipse.jdt.internal.compiler.util.CtSym Maven / Gradle / Ivy

/*******************************************************************************
 * Copyright (c) 2020, 2021 Andrey Loskutov and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     Andrey Loskutov  - initial API and implementation
 *******************************************************************************/
package org.eclipse.jdt.internal.compiler.util;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.URI;
import java.nio.channels.ClosedByInterruptException;
import java.nio.file.DirectoryStream;
import java.nio.file.FileSystem;
import java.nio.file.FileSystemAlreadyExistsException;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.ProviderNotFoundException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Stream;

/**
 * Abstraction to the ct.sym file access (see https://openjdk.java.net/jeps/247). The ct.sym file is required to
 * implement JEP 247 feature (compile with "--release" option against class stubs for older releases) and is currently
 * (Java 15) a jar file with undocumented internal structure, currently existing in at least two different format
 * versions (pre Java 12 and Java 12 and later).
 * 

* The only documentation known seem to be the current implementation of * com.sun.tools.javac.platform.JDKPlatformProvider and probably some JDK build tools that construct ct.sym file. Root * directories inside the file are somehow related to the Java release number, encoded as single digit or letter (single * digits for releases 7 to 9, capital letters for 10 and higher). *

* If a release directory contains "system-modules" file, it is a flag that this release files are not inside ct.sym * file because it is the current release, and jrt file system should be used instead. *

* All other release directories contain encoded signature (*.sig) files with class stubs for classes in the release. *

* Some directories contain files that are shared between different releases, exact logic how they are distributed is * not known. *

* Known format versions of ct.sym: *

* Pre JDK 12: * *

 * ct.sym -> 9 -> java/ -> lang/
 * ct.sym -> 9-modules -> java.base -> module-info.sig
 * 
* * From JDK 12 onward: * *
 * ct.sym -> 9 -> java.base -> java/ -> lang/
 * ct.sym -> 9 -> java.base -> module-info.sig
 * 
* * Notably, *
    *
  1. in JDK 12 modules classes and ordinary classes are located in the same location *
  2. in JDK 12, ordinary classes are found inside their respective modules *
*

* * Searching a file for a given release in ct.sym means finding and traversing all possible release related directories * and searching for matching path. */ public class CtSym { /** * 'B' is code for Java 11, see {@link #getReleaseCode(String)}. */ private static final char JAVA_11 = 'B'; public static final boolean DISABLE_CACHE = Boolean.getBoolean("org.eclipse.jdt.disable_CTSYM_cache"); //$NON-NLS-1$ static boolean VERBOSE = false; /** * Map from path (release) inside ct.sym file to all class signatures loaded */ private final Map> fileCache = new ConcurrentHashMap<>(10007); private final Path jdkHome; private final Path ctSymFile; private FileSystem fs; Path root; private boolean isJRE12Plus; /** * Paths of all root directories, per release (versions encoded). e.g. in JDK 11, Java 10 mapping looks like A -> [A, * A-modules, A789, A9] but to have more fun, in JDK 14, same mapping looks like A -> [A, AB, ABC, ABCD] */ private final Map> releaseRootPaths = new ConcurrentHashMap<>(); /** * All paths that exist in all release root directories, per release (versions encoded). The first key is release * code. The second key is the "full qualified binary name" of the class (without module name and * with .sig suffix). The value is the full path of the corresponding signature file in the ct.sym file. */ private final Map> allReleasesPaths = new ConcurrentHashMap<>(); CtSym(Path jdkHome) throws IOException { this.jdkHome = jdkHome; this.ctSymFile = jdkHome.resolve("lib/ct.sym"); //$NON-NLS-1$ init(); } @SuppressWarnings("resource") // FileSystem must not be closed private void init() throws IOException { boolean exists = Files.exists(this.ctSymFile); if (!exists) { throw new FileNotFoundException("File " + this.ctSymFile + " does not exist"); //$NON-NLS-1$//$NON-NLS-2$ } FileSystem fst = null; URI uri = URI.create("jar:file:" + this.ctSymFile.toUri().getRawPath()); //$NON-NLS-1$ try { fst = FileSystems.getFileSystem(uri); } catch (Exception fne) { // Ignore and move on } if (fst == null) { try { fst = FileSystems.newFileSystem(uri, new HashMap<>(), ClassLoader.getSystemClassLoader()); } catch (FileSystemAlreadyExistsException e) { fst = FileSystems.getFileSystem(uri); } catch (ProviderNotFoundException e) { throw new IOException("Failed to create ct.sym file system for " + this.ctSymFile, e); //$NON-NLS-1$ } } this.fs = fst; if (fst == null) { throw new IOException("Failed to create ct.sym file system for " + this.ctSymFile); //$NON-NLS-1$ } else { this.root = fst.getPath("/"); //$NON-NLS-1$ this.isJRE12Plus = isCurrentRelease12plus(); } } /** * @return never null */ public FileSystem getFs() { return this.fs; } /** * * @return true if this file is from Java 12+ JRE */ public boolean isJRE12Plus() { return this.isJRE12Plus; } /** * @return never null */ public Path getRoot() { return this.root; } /** * @param releaseCode * major JDK version segment as version code (8, 9, A, etc) * @return set with all root paths related to given release in ct.sym file */ public List releaseRoots(String releaseCode) { List list = this.releaseRootPaths.computeIfAbsent(releaseCode, x -> { List rootDirs = new ArrayList<>(); try (DirectoryStream stream = Files.newDirectoryStream(this.root)) { for (final Path subdir : stream) { String rel = subdir.getFileName().toString(); if (rel.contains("-")) { //$NON-NLS-1$ // Ignore META-INF etc. We are only interested in A-Z 0-9 continue; } // com.sun.tools.javac.platform.JDKPlatformProvider.PlatformDescriptionImpl.getFileManager() // https://github.com/openjdk/jdk/blob/master/src/jdk.compiler/share/classes/com/sun/tools/javac/platform/JDKPlatformProvider.java if (rel.contains(releaseCode)) { rootDirs.add(subdir); } else { continue; } } } catch (IOException e) { String error = "Failed to init CtSym for release code " + releaseCode + " and path " + this.root; //$NON-NLS-1$ //$NON-NLS-2$ if (JRTUtil.PROPAGATE_IO_ERRORS) { throw new IllegalStateException(error, e); } else { System.err.println(error); e.printStackTrace(); return Collections.emptyList(); } } return Collections.unmodifiableList(rootDirs); }); return list; } /** * Retrieves the full path in ct.sym file fro given signature file in given release *

* 12+: something like *

* java/io/Reader.sig -> /879/java.base/java/io/Reader.sig *

* before 12: *

* java/io/Reader.sig -> /8769/java/io/Reader.sig * * @param releaseCode release number encoded (7,8,9,A,B...) * @param qualifiedSignatureFileName signature file name (without module) * @return corresponding path in ct.sym file system or null if not found */ public Path getFullPath(String releaseCode, String qualifiedSignatureFileName, String moduleName) { String sep = this.fs.getSeparator(); if (DISABLE_CACHE) { List releaseRoots = releaseRoots(releaseCode); for (Path rroot : releaseRoots) { // Calculate file path Path p = null; if (isJRE12Plus()) { if (moduleName == null) { moduleName = getModuleInJre12plus(releaseCode, qualifiedSignatureFileName); } p = rroot.resolve(moduleName + sep + qualifiedSignatureFileName); } else { p = rroot.resolve(qualifiedSignatureFileName); } // If file is known, read it from ct.sym if (Files.exists(p)) { if (VERBOSE) { System.out.println("found: " + qualifiedSignatureFileName + " in " + p + " for module " + moduleName + "\n"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ } return p; } } if (VERBOSE) { System.out.println("not found: " + qualifiedSignatureFileName + " for module " + moduleName); //$NON-NLS-1$ //$NON-NLS-2$ } return null; } Map releasePaths = getCachedReleasePaths(releaseCode); Path path; if(moduleName != null) { // Without this, org.eclipse.jdt.core.tests.model.ModuleBuilderTests.testConvertToModule() fails on 12+ JRE path = releasePaths.get(moduleName + sep + qualifiedSignatureFileName); // Special handling of broken module schema in java 11 for compilation with --release 9 and --release 10 if(path == null && !this.isJRE12Plus() && ("A".equals(releaseCode) || "9".equals(releaseCode))){ //$NON-NLS-1$ //$NON-NLS-2$ path = releasePaths.get(qualifiedSignatureFileName); } } else { path = releasePaths.get(qualifiedSignatureFileName); } if (VERBOSE) { if (path != null) { System.out.println("found: " + qualifiedSignatureFileName + " in " + path + " for module " + moduleName +"\n"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ } else { System.out.println("not found: " + qualifiedSignatureFileName + " for module " + moduleName); //$NON-NLS-1$ //$NON-NLS-2$ } } return path; } public String getModuleInJre12plus(String releaseCode, String qualifiedSignatureFileName) { if (DISABLE_CACHE) { return findModuleForFileInJre12plus(releaseCode, qualifiedSignatureFileName); } Map releasePaths = getCachedReleasePaths(releaseCode); Path path = releasePaths.get(qualifiedSignatureFileName); if (path != null && path.getNameCount() > 2) { // First segment is release, second: module return path.getName(1).toString(); } return null; } private String findModuleForFileInJre12plus(String releaseCode, String qualifiedSignatureFileName) { for (Path rroot : releaseRoots(releaseCode)) { try (DirectoryStream stream = Files.newDirectoryStream(rroot)) { for (final Path subdir : stream) { Path p = subdir.resolve(qualifiedSignatureFileName); if (Files.exists(p)) { if (subdir.getNameCount() == 2) { return subdir.getName(1).toString(); } } } } catch (IOException e) { String error = "Failed to read directory " + rroot + " contents in " + this.root; //$NON-NLS-1$ //$NON-NLS-2$ if (JRTUtil.PROPAGATE_IO_ERRORS) { throw new IllegalStateException(error, e); } else { System.err.println(error); e.printStackTrace(); } } } return null; } /** * Populates {@link #allReleasesPaths} with the paths of all files within each matching release directory in ct.sym. * This cache is an optimization to avoid excessive calls into the zip filesystem in * {@code ClasspathJrtWithReleaseOption#findClass(String, String, String, String, boolean, Predicate)}. *

* 12+: something like *

* java.base/javax/net/ssl/SSLSocketFactory.sig -> /89ABC/java.base/javax/net/ssl/SSLSocketFactory.sig *

or * javax/net/ssl/SSLSocketFactory.sig -> /89ABC/java.base/javax/net/ssl/SSLSocketFactory.sig *

* before 12: javax/net/ssl/SSLSocketFactory.sig -> /89ABC/java.base/javax/net/ssl/SSLSocketFactory.sig */ private Map getCachedReleasePaths(String releaseCode) { Map result = this.allReleasesPaths.computeIfAbsent(releaseCode, x -> { List roots = releaseRoots(releaseCode); Map allReleaseFiles = new HashMap<>(4999); for (Path start : roots) { try (Stream fileStream=Files.walk(start)) { fileStream.filter(Files::isRegularFile).forEach(p -> { if (isJRE12Plus()) { // Don't use module name as part of the key String binaryNameWithoutModule = p.subpath(2, p.getNameCount()).toString(); allReleaseFiles.put(binaryNameWithoutModule, p); // Cache extra key with module added, see getFullPath(). String binaryNameWithModule = p.subpath(1, p.getNameCount()).toString(); allReleaseFiles.put(binaryNameWithModule, p); } else { String binaryNameWithoutModule = p.subpath(1, p.getNameCount()).toString(); allReleaseFiles.put(binaryNameWithoutModule, p); } }); } catch (IOException e) { String error = "Failed to read directory " + start + " contents in " + this.root; //$NON-NLS-1$ //$NON-NLS-2$ if (JRTUtil.PROPAGATE_IO_ERRORS) { throw new IllegalStateException(error, e); } else { // Not much do to if we can't list the dir; anything in there will be treated // as if it were missing. System.err.println(error); e.printStackTrace(); } } } return Collections.unmodifiableMap(allReleaseFiles); }); return result; } public byte[] getFileBytes(Path path) throws IOException { if (DISABLE_CACHE) { return JRTUtil.safeReadBytes(path); } else { Optional bytes = this.fileCache.computeIfAbsent(path, key -> { try { return Optional.ofNullable(JRTUtil.safeReadBytes(key)); } catch (ClosedByInterruptException e) { // Don't cache return null; } catch (IOException e) { // remember there is nothing to return return Optional.empty(); } }); if (VERBOSE) { System.out.println("got bytes: " + path); //$NON-NLS-1$ } return bytes == null ? null : bytes.orElse(null); } } private boolean isCurrentRelease12plus() throws IOException { // ignore everything that is not one character (Java release code is one character plus separator) try (DirectoryStream stream = Files.newDirectoryStream(this.root, p -> p.toString().length() == 2)) { for (final Path subdir : stream) { String rel = JRTUtil.sanitizedFileName(subdir); if (rel.length() != 1) { continue; } try { char releaseCode = rel.charAt(0); // If any release directory letter is higher 11 we are fine if (releaseCode > JAVA_11) { return true; } } catch (NumberFormatException e) { // META-INF, A-modules etc continue; } } } return false; } @Override public int hashCode() { return this.jdkHome.hashCode(); } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (!(obj instanceof CtSym)) { return false; } CtSym other = (CtSym) obj; return this.jdkHome.equals(other.jdkHome); } @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append("CtSym ["); //$NON-NLS-1$ sb.append("file="); //$NON-NLS-1$ sb.append(this.ctSymFile); sb.append("]"); //$NON-NLS-1$ return sb.toString(); } /** * Tries to translate numeric Java version to the corresponding release "code". *

    *
  • 7, 8 and 9 are just returned "as is" *
  • versions up from 10 are returned as upper letters starting with "A", so 10 is "A", 11 is "B" and so on. *
* * @param release * release version as number (8, 9, 10, ...) * @return the "code" used by ct.sym for given Java version */ public static String getReleaseCode(String release) { int numericVersion = Integer.parseInt(release); if(numericVersion < 10) { return String.valueOf(numericVersion); } return String.valueOf((char) ('A' + (numericVersion - 10))); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy