From afd54cf4d9cda25469e2913920603788c28c648c Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Wed, 7 Jul 2021 13:28:04 +0200 Subject: [PATCH] Allow TypeSystem to scan dependencies. --- .../nativex/type/TypeSystem.java | 211 +++++++++++++++++- 1 file changed, 210 insertions(+), 1 deletion(-) diff --git a/spring-aot/src/main/java/org/springframework/nativex/type/TypeSystem.java b/spring-aot/src/main/java/org/springframework/nativex/type/TypeSystem.java index 4c7f6fdc6..8e559255e 100644 --- a/spring-aot/src/main/java/org/springframework/nativex/type/TypeSystem.java +++ b/spring-aot/src/main/java/org/springframework/nativex/type/TypeSystem.java @@ -484,6 +484,215 @@ public byte[] find(String slashedTypeName) { } } + /** + * Prepare a {@link TypeScanner} selecting (single class files or jars) for {@link Type} scanning. + * + * @param filter must not be {@literal null}. + * @return new instance of {@link TypeScanner}. + */ + public TypeScanner scanFiles(Predicate filter) { + return new TypeScanner(this).files(filter); + } + + /** + * Prepare a {@link TypeScanner} selecting specific dependencies (.jar files) for {@link Type} scanning.
+ * + *
+ *
log4j-core
+ *
matches any version of the given artifact name
+ *
log4j-core-2.1.4.jar*
+ *
artifact names ending with .jar match against the exact file name.
+ *
log4j*
+ *
the * postfix will scan for all artifacts staring with the given name.
+ *
+ * + * @param artifactNames must not be {@literal null}. + * @return new instance of {@link TypeScanner}. + */ + public TypeScanner scanDependencies(String... artifactNames) { + + TypeScanner scanner = new TypeScanner(this).files(file -> { + + boolean found = false; + + String artifactNameFromFile = TypeScanner.extractArtifactNameFromFile(file).toLowerCase(); + + for (String artifactName : artifactNames) { + if (artifactName.endsWith("*")) { + found = artifactNameFromFile.startsWith(artifactName.replace("*", "").toLowerCase()); + } else if (artifactName.endsWith(".jar")) { + found = file.getName().equalsIgnoreCase(artifactName); + } else { + found = artifactNameFromFile.equals(artifactName.toLowerCase()); + } + if (found) { + break; + } + } + return found; + }); + + scanner.includeAppPackages = false; // TODO: would you expect bits of the app to be scanned? I don't think so. + return scanner; + } + + /** + * Utility for discovering {@link Type types} within dependencies + */ + public static class TypeScanner { + + private final TypeSystem typeSystem; + private boolean cacheResults = false; + private boolean includeAppPackages = true; + private Predicate fileFilter = (it) -> true; + private Predicate typeFilter = (it) -> true; + + TypeScanner(TypeSystem typeSystem) { + this.typeSystem = typeSystem; + } + + TypeScanner files(Predicate filter) { + + this.fileFilter = filter; + return this; + } + + /** + * Include types matching the given {@link Predicate filter}. + * + * @param filter must not be {@literal null}. + * @return this. + */ + public TypeScanner forTypes(Predicate filter) { + + this.typeFilter = filter; + return this; + } + + /** + * Put discovered {@link Type types} into the overall {@link TypeSystem} cache. + * Use with caution! + * + * @return this. + */ + public TypeScanner cacheResults() { + + cacheResults = true; + return this; + } + + /** + * Start looking for {@link File files} matching the given {@link Predicate filter} and inspect discovered {@link Type types}. + * + * @return never {@literal null}. + */ + public Stream stream() { + + return Stream.concat( + typeSystem.appPackages.values() // the list of pages + .stream() + .filter(it -> includeAppPackages) + .flatMap(it -> it.stream()) // get the individual Files + .distinct() + .filter(file -> !file.getName().contains("module-info") && !file.getName().contains("package-info")) + .filter(fileFilter) + .flatMap(file -> { + return readTypes(file); + }), + typeSystem.packageCache.values() + .stream() + .flatMap(it -> it.stream()) + .distinct() + .filter(fileFilter) + .flatMap(jarFile -> { + return readTypesFromJar(jarFile); + })) + .distinct() + .filter(typeFilter) + .map(it -> { + + // Cache, should we? + if (cacheResults) { + typeSystem.typeCache.putIfAbsent(it.getName(), it); + } + return it; + }); + } + + /** + * Collect {@link Type types} from {@link File files} matching the given {@link Predicate filter}. + * + * @return never {@literal null}. + */ + public List list() { + return stream().collect(Collectors.toList()); + } + + private Stream readTypes(File file) { + + if (file.isDirectory()) { + return Arrays.stream(file.listFiles()).flatMap(this::readTypes); + } else if (file.getName().endsWith(".class")) { + try { + byte[] bytes = Files.readAllBytes(Paths.get(file.toURI())); + return Stream.of(typeForNode(new ClassReader(bytes))); + } catch (IOException ioe) { + throw new IllegalStateException(ioe); + } + } + return Stream.empty(); + } + + private Stream readTypesFromJar(File zipFile) { + + Collection types = new ArrayList<>(); + + try (ZipFile zf = new ZipFile(zipFile)) { + Enumeration entries = zf.entries(); + while (entries.hasMoreElements()) { + ZipEntry entry = entries.nextElement(); + if (entry.getName().endsWith(".class") && !entry.getName().contains("module-info") && !entry.getName().contains("package-info")) { + types.add(typeForNode(new ClassReader(zf.getInputStream(entry)))); + } + } + } catch (IOException ioe) { + throw new IllegalStateException(ioe); + } + + return types.stream(); + } + + /** + * Remove the file {@literal .jar} file type extension and potential version identifiers. + * + *
    + *
  • log4j.jar -> log4j
  • + *
  • log-4-j.jar -> log-4-j
  • + *
  • log4j-1.3.0-SNAPSHOT.jar -> log4j
  • + *
  • log-4-j-1.4.0.jar -> log-4-j
  • + *
  • log4j-1.4.0.jar -> log4j
  • + *
  • log4j-1.5.0.M1 -> log4j
  • + *
+ * @param file + * @return + */ + static String extractArtifactNameFromFile(File file) { + return file.getName().replaceAll("\\.jar|-(!?\\d\\.).*[\\w\\d]", ""); + } + + Type typeForNode(ClassReader reader) { + + ClassNode node = new ClassNode(); + reader.accept(node, ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES); + + if (node == null && typeSystem.typeCache.containsKey(node.name)) { + return typeSystem.typeCache.get(node.name); + } + + return Type.forClassNode(this.typeSystem, node, 0); + } + } + public static byte[] loadFromStream(InputStream stream) { try { BufferedInputStream bis = new BufferedInputStream(stream); @@ -1577,4 +1786,4 @@ public boolean exists(TypeName typename) { return resolvedType != null; } -} \ No newline at end of file +}