Skip to content

Commit

Permalink
TypeTable as a substitute for packing whole jars into `META-INF/rew…
Browse files Browse the repository at this point in the history
…rite/classpath` (#4932)
  • Loading branch information
jkschneider authored Jan 22, 2025
1 parent 11a0924 commit 7661418
Show file tree
Hide file tree
Showing 8 changed files with 876 additions and 103 deletions.
6 changes: 3 additions & 3 deletions rewrite-java/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ tasks.register<JavaExec>("generateAntlrSources") {
mainClass.set("org.antlr.v4.Tool")

args = listOf(
"-o", "src/main/java/org/openrewrite/java/internal/grammar",
"-package", "org.openrewrite.java.internal.grammar",
"-visitor"
"-o", "src/main/java/org/openrewrite/java/internal/grammar",
"-package", "org.openrewrite.java.internal.grammar",
"-visitor"
) + fileTree("src/main/antlr").matching { include("**/*.g4") }.map { it.path }

classpath = antlrGeneration
Expand Down
134 changes: 34 additions & 100 deletions rewrite-java/src/main/java/org/openrewrite/java/JavaParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,34 +16,29 @@
package org.openrewrite.java;

import io.github.classgraph.ClassGraph;
import io.github.classgraph.Resource;
import io.github.classgraph.ResourceList;
import io.github.classgraph.ScanResult;
import org.intellij.lang.annotations.Language;
import org.jspecify.annotations.Nullable;
import org.openrewrite.*;
import org.openrewrite.java.internal.JavaTypeCache;
import org.openrewrite.java.internal.parser.JavaParserClasspathLoader;
import org.openrewrite.java.internal.parser.RewriteClasspathJarClasspathLoader;
import org.openrewrite.java.internal.parser.TypeTable;
import org.openrewrite.java.marker.JavaSourceSet;
import org.openrewrite.java.tree.J;
import org.openrewrite.style.NamedStyles;

import java.io.*;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.io.ByteArrayInputStream;
import java.net.URI;
import java.nio.charset.Charset;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;

import static java.util.Objects.requireNonNull;
import static java.util.Collections.emptyList;
import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toList;

Expand Down Expand Up @@ -95,7 +90,7 @@ static List<Path> dependenciesFromClasspath(String... artifactNames) {
/**
* Filters the classpath entries to find paths that match the given artifact name.
*
* @param artifactName The artifact name to search for.
* @param artifactName The artifact name to search for.
* @param runtimeClasspath The list of classpath URIs to search within.
* @return List of Paths that match the artifact name.
*/
Expand Down Expand Up @@ -125,94 +120,32 @@ static List<Path> filterArtifacts(String artifactName, List<URI> runtimeClasspat

static List<Path> dependenciesFromResources(ExecutionContext ctx, String... artifactNamesWithVersions) {
if (artifactNamesWithVersions.length == 0) {
return Collections.emptyList();
return emptyList();
}
List<Path> artifacts = new ArrayList<>(artifactNamesWithVersions.length);
Set<String> missingArtifactNames = new LinkedHashSet<>(artifactNamesWithVersions.length);
missingArtifactNames.addAll(Arrays.asList(artifactNamesWithVersions));
File resourceTarget = JavaParserExecutionContextView.view(ctx)
.getParserClasspathDownloadTarget();

Class<?> caller;
try {
// StackWalker is only available in Java 15+, but right now we only use classloader isolated
// recipe instances in Java 17 environments, so we can safely use StackWalker there.
Class<?> options = Class.forName("java.lang.StackWalker$Option");
Object retainOption = options.getDeclaredField("RETAIN_CLASS_REFERENCE").get(null);

Class<?> walkerClass = Class.forName("java.lang.StackWalker");
Method getInstance = walkerClass.getDeclaredMethod("getInstance", options);
Object walker = getInstance.invoke(null, retainOption);
Method getDeclaringClass = Class.forName("java.lang.StackWalker$StackFrame").getDeclaredMethod("getDeclaringClass");
caller = (Class<?>) walkerClass.getMethod("walk", Function.class).invoke(walker, (Function<Stream<Object>, Object>) s -> s
.map(f -> {
try {
return (Class<?>) getDeclaringClass.invoke(f);
} catch (IllegalAccessException | InvocationTargetException e) {
throw new RuntimeException(e);
}
})
// Drop anything before the parser or builder class, as well as those classes themselves
.filter(new Predicate<Class<?>>() {
boolean parserOrBuilderFound = false;

@Override
public boolean test(Class<?> c1) {
if (c1.getName().equals(JavaParser.class.getName()) ||
c1.getName().equals(Builder.class.getName())) {
parserOrBuilderFound = true;
return false;
}
return parserOrBuilderFound;
}
})
.findFirst()
.orElseThrow(() -> new IllegalStateException("Unable to find caller of JavaParser.dependenciesFromResources(..)")));
} catch (ClassNotFoundException | NoSuchFieldException | IllegalAccessException |
NoSuchMethodException | InvocationTargetException e) {
caller = JavaParser.class;
}

try (ScanResult result = new ClassGraph().acceptPaths("META-INF/rewrite/classpath")
.addClassLoader(caller.getClassLoader())
.scan()) {
ResourceList resources = result.getResourcesWithExtension(".jar");

for (String artifactName : new ArrayList<>(missingArtifactNames)) {
Pattern jarPattern = Pattern.compile(artifactName + "-?.*\\.jar$");
for (Resource resource : resources) {
if (jarPattern.matcher(resource.getPath()).find()) {
try {
Path artifact = resourceTarget.toPath().resolve(Paths.get(resource.getPath()).getFileName());
if (!Files.exists(artifact)) {
try {
InputStream resourceAsStream = requireNonNull(
caller.getResourceAsStream("/" + resource.getPath()),
caller.getCanonicalName() + " resource not found: " + resource.getPath());
Files.copy(resourceAsStream, artifact);
} catch (FileAlreadyExistsException ignore) {
// can happen when tests run in parallel, for example
}
}
missingArtifactNames.remove(artifactName);
artifacts.add(artifact);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
break;
Set<String> missingArtifactNames = new LinkedHashSet<>(Arrays.asList(artifactNamesWithVersions));

try (RewriteClasspathJarClasspathLoader rewriteClasspathJarClasspathLoader = new RewriteClasspathJarClasspathLoader(ctx)) {
List<JavaParserClasspathLoader> loaders = new ArrayList<>(2);
Optional.ofNullable(TypeTable.fromClasspath(ctx, missingArtifactNames)).ifPresent(loaders::add);
loaders.add(rewriteClasspathJarClasspathLoader);

for (JavaParserClasspathLoader loader : loaders) {
for (String missingArtifactName : new ArrayList<>(missingArtifactNames)) {
Path located = loader.load(missingArtifactName);
if (located != null) {
artifacts.add(located);
missingArtifactNames.remove(missingArtifactName);
}
}
}
}

if (!missingArtifactNames.isEmpty()) {
throw new IllegalArgumentException(
"Unable to find classpath resource dependencies beginning with: " +
missingArtifactNames.stream().map(a -> "'" + a + "'").sorted().collect(joining(", ", "", ".\n")) +
"The caller is of type " + caller.getName() + ".\n" +
"The resources resolvable from the caller's classpath are: " +
resources.stream().map(Resource::getPath).sorted().collect(joining(", "))
);
}
if (!missingArtifactNames.isEmpty()) {
throw new IllegalArgumentException(
"Unable to find classpath resource dependencies beginning with: " +
missingArtifactNames.stream().map(a -> "'" + a + "'").sorted().collect(joining(", ", "", ".\n"))
);
}

return artifacts;
Expand Down Expand Up @@ -324,9 +257,9 @@ default boolean accept(Path path) {

@SuppressWarnings("unchecked")
abstract class Builder<P extends JavaParser, B extends Builder<P, B>> extends Parser.Builder {
protected Collection<Path> classpath = Collections.emptyList();
protected Collection<String> artifactNames = Collections.emptyList();
protected Collection<byte[]> classBytesClasspath = Collections.emptyList();
protected Collection<Path> classpath = emptyList();
protected Collection<String> artifactNames = emptyList();
protected Collection<byte[]> classBytesClasspath = emptyList();
protected JavaTypeCache javaTypeCache = new JavaTypeCache();

@Nullable
Expand Down Expand Up @@ -369,7 +302,7 @@ public B dependsOn(@Language("java") String... inputsAsStrings) {
}

public B classpath(Collection<Path> classpath) {
this.artifactNames = Collections.emptyList();
this.artifactNames = emptyList();
this.classpath = classpath;
return (B) this;
}
Expand All @@ -388,13 +321,13 @@ public B addClasspathEntry(Path entry) {

public B classpath(String... artifactNames) {
this.artifactNames = Arrays.asList(artifactNames);
this.classpath = Collections.emptyList();
this.classpath = emptyList();
return (B) this;
}

@SuppressWarnings({"UnusedReturnValue", "unused"})
public B classpathFromResources(ExecutionContext ctx, String... classpath) {
this.artifactNames = Collections.emptyList();
this.artifactNames = emptyList();
this.classpath = dependenciesFromResources(ctx, classpath);
return (B) this;
}
Expand All @@ -415,7 +348,7 @@ protected Collection<Path> resolvedClasspath() {
if (!artifactNames.isEmpty()) {
classpath = new ArrayList<>(classpath);
classpath.addAll(JavaParser.dependenciesFromClasspath(artifactNames.toArray(new String[0])));
artifactNames = Collections.emptyList();
artifactNames = emptyList();
}
return classpath;
}
Expand Down Expand Up @@ -483,3 +416,4 @@ static List<Path> getRuntimeClasspath() {
return runtimeClasspath;
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*
* Copyright 2025 the original author or authors.
* <p>
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* https://www.apache.org/licenses/LICENSE-2.0
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openrewrite.java.internal.parser;

import org.openrewrite.ExecutionContext;
import org.openrewrite.java.JavaParser;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Stream;

/**
* This class is used to find the caller of {@link JavaParser#dependenciesFromResources(ExecutionContext, String...)},
* which is used to load classpath resources for {@link JavaParser}.
*/
class JavaParserCaller {
private JavaParserCaller() {
}

public static Class<?> findCaller() {
Class<?> caller;
try {
// StackWalker is only available in Java 15+, but right now we only use classloader isolated
// recipe instances in Java 17 environments, so we can safely use StackWalker there.
Class<?> options = Class.forName("java.lang.StackWalker$Option");
Object retainOption = options.getDeclaredField("RETAIN_CLASS_REFERENCE").get(null);

Class<?> walkerClass = Class.forName("java.lang.StackWalker");
Method getInstance = walkerClass.getDeclaredMethod("getInstance", options);
Object walker = getInstance.invoke(null, retainOption);
Method getDeclaringClass = Class.forName("java.lang.StackWalker$StackFrame").getDeclaredMethod("getDeclaringClass");
caller = (Class<?>) walkerClass.getMethod("walk", Function.class).invoke(walker, (Function<Stream<Object>, Object>) s -> s
.map(f -> {
try {
return (Class<?>) getDeclaringClass.invoke(f);
} catch (IllegalAccessException | InvocationTargetException e) {
throw new RuntimeException(e);
}
})
// Drop anything before the parser or builder class, as well as those classes themselves
.filter(new Predicate<Class<?>>() {
boolean parserOrBuilderFound = false;

@Override
public boolean test(Class<?> c1) {
if (c1.getName().equals(JavaParser.class.getName()) ||
c1.getName().equals(JavaParser.Builder.class.getName())) {
parserOrBuilderFound = true;
return false;
}
return parserOrBuilderFound;
}
})
.findFirst()
.orElseThrow(() -> new IllegalStateException("Unable to find caller of JavaParser.dependenciesFromResources(..)")));
} catch (ClassNotFoundException | NoSuchFieldException | IllegalAccessException |
NoSuchMethodException | InvocationTargetException e) {
caller = JavaParser.class;
}
return caller;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* Copyright 2025 the original author or authors.
* <p>
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* https://www.apache.org/licenses/LICENSE-2.0
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openrewrite.java.internal.parser;

import org.jspecify.annotations.Nullable;
import org.openrewrite.ExecutionContext;
import org.openrewrite.java.JavaParser;

import java.nio.file.Path;

/**
* Prepares classpath resources for use by {@link JavaParser}.
*/
public interface JavaParserClasspathLoader {

/**
* Load a classpath resource.
*
* @param artifactName A descriptor for the classpath resource to load.
* @return The path a JAR or classes directory that is suitable for use
* as a classpath entry in a compilation step.
*/
@Nullable
Path load(String artifactName);
}
Loading

0 comments on commit 7661418

Please sign in to comment.