How to enrich ClassRealm in mvn plugin? - java

I'm writing my own maven plugin, and I have an issue to load a certain class. This post proposed a way to enrich the ClassRealm to broad class loading scope.
#Mojo(
name = "deploy",
defaultPhase = LifecyclePhase.DEPLOY,
requiresDependencyCollection = ResolutionScope.RUNTIME,
requiresDirectInvocation = true,
requiresOnline = true
)
public class DeployMojo extends AbstractMojo {
#Parameter
private String server;
#Parameter(defaultValue = "${project}", readonly = true, required = true)
private MavenProject project;
#Component
private PluginDescriptor descriptor;
public void execute() throws MojoExecutionException, MojoFailureException {
// Added runtime resources for the project and create the classloader
final var realm = descriptor.getClassRealm();
final ArrayList<String> classpathElements;
try {
classpathElements = new ArrayList<>(project.getRuntimeClasspathElements());
} catch (DependencyResolutionRequiredException e) {
throw new MojoExecutionException("Unable to resolve project dependencies", e);
}
classpathElements.add(project.getBuild().getOutputDirectory());
final var urls = new URL[classpathElements.size()];
for (int i = 0; i < classpathElements.size(); ++i) {
try {
urls[i] = new File(classpathElements.get(i)).toURI().toURL();
realm.addURL(urls[i]);
} catch (MalformedURLException e) {
throw new MojoExecutionException(String.format("Unable to parse classpath: %s as URL", classpathElements.get(i)), e);
}
}
//Some other operations
}
}
However, when I try to get the ClassRealm via descriptor.getClassRealm(), it shows that Cannot access org.codehaus.plexus.classworlds.realm.ClassRealm. Also in the documentation, it mentions that Warning: This is an internal utility method that is only public for technical reasons, it is not part of the public API. In particular, this method can be changed or deleted without prior notice and must not be used by plugins.
I wonder is there a way to enrich the ClassRealm, or this is something that we shouldn't change.

Related

URI for third party dependency

My Question:
Is it possible to get a Uri from an import of a third party dependency?
Question Context:
I am trying to get a list of classes accessed with the following.
import com.name.*
In the context in which I want to use it, I will not be able to use third party dependencies. I do however need to find all classes associated with a third party dependency import.
I have found one such answer to my issue in the following code, provided by the user tirz.
public static List<Class<?>> getClassesForPackage(final String pkgName) throws IOException, URISyntaxException {
final String pkgPath = pkgName.replace('.', '/');
final URI pkg = Objects.requireNonNull(ClassLoader.getSystemClassLoader().getResource(pkgPath)).toURI();
final ArrayList<Class<?>> allClasses = new ArrayList<Class<?>>();
Path root;
if (pkg.toString().startsWith("jar:")) {
try {
root = FileSystems.getFileSystem(pkg).getPath(pkgPath);
} catch (final FileSystemNotFoundException e) {
root = FileSystems.newFileSystem(pkg, Collections.emptyMap()).getPath(pkgPath);
}
} else {
root = Paths.get(pkg);
}
final String extension = ".class";
try (final Stream<Path> allPaths = Files.walk(root)) {
allPaths.filter(Files::isRegularFile).forEach(file -> {
try {
final String path = file.toString().replace('/', '.');
final String name = path.substring(path.indexOf(pkgName), path.length() - extension.length());
allClasses.add(Class.forName(name));
} catch (final ClassNotFoundException | StringIndexOutOfBoundsException ignored) {
}
});
}
return allClasses;
}
The problem I have with the code above is that where final URI pkg is assigned. This works with a package that exists within the project, but if an import for a third party dependency is used this throws a NullPointerException. Is it possible to make this code work for third party dependencies? Might this require some reference to an .m2 folder or other library resource?

List available versions from sonatype nexus with aether-api

my goal is to develop an internal tool for artifact deploying. The artifacts are located in a local repository management system (sonatype nexus). After researching, I tried to implement the task, with Aether-Api. But I failed at reprogramming one of their examples at my own. I can't evaluate the error.
public class SourceMaven
{
private static RepositorySystem newRepositorySystem()
{
DefaultServiceLocator locator = MavenRepositorySystemUtils.newServiceLocator();
locator.addService(RepositoryConnectorFactory.class, BasicRepositoryConnectorFactory.class);
locator.addService(TransporterFactory.class, FileTransporterFactory.class);
locator.addService(TransporterFactory.class, HttpTransporterFactory.class);
return locator.getService(RepositorySystem.class);
}
private static RepositorySystemSession newSession(RepositorySystem system)
{
DefaultRepositorySystemSession session = MavenRepositorySystemUtils.newSession();
LocalRepository localRepo = new LocalRepository("/usr/local/home/myusername/tmp/aether");
session.setLocalRepositoryManager(system.newLocalRepositoryManager(session, localRepo));
return session;
}
private static List<RemoteRepository> newRepositories( RepositorySystem system, RepositorySystemSession session)
{
return new ArrayList<RemoteRepository>(Arrays.asList(newCentralRepository()));
}
private static RemoteRepository newCentralRepository()
{
return new RemoteRepository.Builder("sonanexus", "default", "http://ournexusservername:8081/nexus/#nexus").build();
}
public List<String> getReleaseList(String url)
{
RepositorySystem system = newRepositorySystem();
RepositorySystemSession session = newSession(system);
Artifact artifact = new DefaultArtifact("org.eclipse.aether:aether-util:[0,)");
VersionRangeRequest rangeRequest = new VersionRangeRequest();
rangeRequest.setArtifact(artifact);
rangeRequest.setRepositories(newRepositories(system, session));
try
{
VersionRangeResult rangeResult = system.resolveVersionRange(session, rangeRequest);
List<Version> versions = rangeResult.getVersions();
System.out.println("available versions " + versions);
}
catch (VersionRangeResolutionException ex)
{
System.out.println("failed ...");
}
return null;
}
}
As output I only get an empty List without an error.
available versions []
The requestested artifact coordinates are linked in our nexus and can be found in the webinterface.
Code above is working.
It is important to clarify the url more specific so instead of
http://hostname:8081/nexus/#nexus
i needed to change it to:
http://hostname:8081/nexus/content/groups/public

Accessing classes in custom maven reporting plugin

I've written a custom maven reporting plugin to output some basic information about spring-mvc classes. In my internal tests I can see that code like this :
public Set<Class<?>> findControllerClasses(File buildOutputDir) throws IOException, ClassNotFoundException {
Collection<URL> urls = ClasspathHelper.forJavaClassPath();
if (buildOutputDir != null) {
urls.add(buildOutputDir.toURI().toURL());
}
Reflections reflections = new Reflections(new ConfigurationBuilder().setUrls(urls));
Set<Class<?>> types = reflections.getTypesAnnotatedWith(Controller.class);
return types;
}
Works well at pulling in annotated classes. However, when I use the reporting plugin in another project, annotated classes are not picked up.
Can someone shed some light on how to access the compiled classes for reporting purposes? Or whether this is even possible ??
EDIT : partially solved using the answer to: Add maven-build-classpath to plugin execution classpath
However, this only loads classes if they have no dependencies outside the runtimeClasspathElements var for maven. Is there any way to merge these classes in to the classrealm too ?
Ok. Expanding on the answer in the above comment, the full solution is to use a Configurer that takes into account both the runtime classpath AND the urls from the poms dependencies. Code shown below
/**
*
* #plexus.component
* role="org.codehaus.plexus.component.configurator.ComponentConfigurator"
* role-hint="include-project-dependencies"
* #plexus.requirement role=
* "org.codehaus.plexus.component.configurator.converters.lookup.ConverterLookup"
* role-hint="default"
*
*/
public class ClassRealmConfigurator extends AbstractComponentConfigurator {
private final static Logger logger = Logger.getLogger(ClassRealmConfigurator.class.getName());
public void configureComponent(Object component, PlexusConfiguration configuration, ExpressionEvaluator expressionEvaluator, ClassRealm containerRealm, ConfigurationListener listener) throws ComponentConfigurationException {
addProjectDependenciesToClassRealm(expressionEvaluator, containerRealm);
converterLookup.registerConverter(new ClassRealmConverter(containerRealm));
ObjectWithFieldsConverter converter = new ObjectWithFieldsConverter();
converter.processConfiguration(converterLookup, component, containerRealm.getClassLoader(), configuration, expressionEvaluator, listener);
}
private void addProjectDependenciesToClassRealm(ExpressionEvaluator expressionEvaluator, ClassRealm containerRealm) throws ComponentConfigurationException {
Set<String> runtimeClasspathElements = new HashSet<String>();
try {
runtimeClasspathElements.addAll((List<String>) expressionEvaluator.evaluate("${project.runtimeClasspathElements}"));
} catch (ExpressionEvaluationException e) {
throw new ComponentConfigurationException("There was a problem evaluating: ${project.runtimeClasspathElements}", e);
}
Collection<URL> urls = buildURLs(runtimeClasspathElements);
urls.addAll(buildAritfactDependencies(expressionEvaluator));
for (URL url : urls) {
containerRealm.addConstituent(url);
}
}
private Collection<URL> buildAritfactDependencies(ExpressionEvaluator expressionEvaluator) throws ComponentConfigurationException {
MavenProject project;
try {
project = (MavenProject) expressionEvaluator.evaluate("${project}");
} catch (ExpressionEvaluationException e1) {
throw new ComponentConfigurationException("There was a problem evaluating: ${project}", e1);
}
Collection<URL> urls = new ArrayList<URL>();
for (Object a : project.getArtifacts()) {
try {
urls.add(((Artifact) a).getFile().toURI().toURL());
} catch (MalformedURLException e) {
throw new ComponentConfigurationException("Unable to resolve artifact dependency: " + a, e);
}
}
return urls;
}
private Collection<URL> buildURLs(Set<String> runtimeClasspathElements) throws ComponentConfigurationException {
List<URL> urls = new ArrayList<URL>(runtimeClasspathElements.size());
for (String element : runtimeClasspathElements) {
try {
final URL url = new File(element).toURI().toURL();
urls.add(url);
} catch (MalformedURLException e) {
throw new ComponentConfigurationException("Unable to access project dependency: " + element, e);
}
}
return urls;
}
}
Maybe use
/**
* The classpath elements of the project.
*
* #parameter expression="${project.runtimeClasspathElements}"
* #required
* #readonly
*/
private List<String> classpathElements;
with
private ClassLoader getProjectClassLoader()
throws DependencyResolutionRequiredException, MalformedURLException
{
List<String> classPath = new ArrayList<String>();
classPath.addAll( classpathElements );
classPath.add( project.getBuild().getOutputDirectory() );
URL[] urls = new URL[classPath.size()];
int i = 0;
for ( String entry : classPath )
{
getLog().debug( "use classPath entry " + entry );
urls[i] = new File( entry ).toURI().toURL();
i++; // Important
}
return new URLClassLoader( urls );
}

How can a Maven plugin discover its own version during execution?

I'd like to be able to discover the version of my plugin during its execution; 0.0.1-SNAPSHOT, 0.0.1, 1.0-SNAPSHOT, etc.
Can this be done? The AbstractMojo class doesn't really give you much information about the plugin itself.
EDIT - I am using the following code as a workaround. It assumes that the MANIFEST for the plugin can be loaded from a resource URL built using the resource URL of the plugin itself. It's not nice but seems to work for MANIFEST located in either file or jar class loader:
String getPluginVersion() throws IOException {
Manifest mf = loadManifest(getClass().getClassLoader(), getClass());
return mf.getMainAttributes().getValue("Implementation-Version");
}
Manifest loadManifest(final ClassLoader cl, final Class c) throws IOException {
String resourceName = "/" + c.getName().replaceAll("\\.", "/") + ".class";
URL classResource = cl.getResource(resourceName);
String path = classResource.toString();
int idx = path.indexOf(resourceName);
if (idx < 0) {
return null;
}
String urlStr = classResource.toString().substring(0, idx) + "/META-INF/MANIFEST.MF";
URL url = new URL(urlStr);
InputStream in = null;
Manifest mf = null;
try {
in = url.openStream();
mf = new Manifest(in);
} finally {
if (null != in) {
in.close();
}
in = null;
}
return mf;
}
I don't think your "workaround" with the manifest file is such a bad idea. Since it's packed inside the .jar of your plugin you should always have access to it.
For this post to be an answer, here is another idea: Let maven do the dirty work for you during the build of your plugin: have a placeholder in your plugin source:
private final String myVersion = "[CURRENT-VERSION]";
use ant-plugin or something else to replace that placeholder with the current version before compilation.
First, add the following dependency to your plugin's POM:
<dependency>
<groupId>org.apache.maven</groupId>
<artifactId>maven-project</artifactId>
<version>2.0</version>
</dependency>
Then you can just do the following:
public class MyMojo extends AbstractMojo {
private static final String GROUP_ID = "your-group-id";
private static final String ARTIFACT_ID = "your-artifact-id";
/**
* #parameter default-value="${project}"
*/
MavenProject project;
public void execute() throws MojoExecutionException {
Set pluginArtifacts = project.getPluginArtifacts();
for (Iterator iterator = pluginArtifacts.iterator(); iterator.hasNext();) {
Artifact artifact = (Artifact) iterator.next();
String groupId = artifact.getGroupId();
String artifactId = artifact.getArtifactId();
if (groupId.equals(GROUP_ID) && artifactId.equals(ARTIFACT_ID)) {
System.out.println(artifact.getVersion());
break;
}
}
}

Getting all Classes from a Package

Lets say I have a java package commands which contains classes that all inherit from ICommand can I get all of those classes somehow? I'm locking for something among the lines of:
Package p = Package.getPackage("commands");
Class<ICommand>[] c = p.getAllPackagedClasses(); //not real
Is something like that possible?
Here's a basic example, assuming that classes are not JAR-packaged:
// Prepare.
String packageName = "com.example.commands";
List<Class<ICommand>> commands = new ArrayList<Class<ICommand>>();
URL root = Thread.currentThread().getContextClassLoader().getResource(packageName.replace(".", "/"));
// Filter .class files.
File[] files = new File(root.getFile()).listFiles(new FilenameFilter() {
public boolean accept(File dir, String name) {
return name.endsWith(".class");
}
});
// Find classes implementing ICommand.
for (File file : files) {
String className = file.getName().replaceAll(".class$", "");
Class<?> cls = Class.forName(packageName + "." + className);
if (ICommand.class.isAssignableFrom(cls)) {
commands.add((Class<ICommand>) cls);
}
}
Below is an implementation using the JSR-199 API, i.e. classes from javax.tools.*:
List<Class> commands = new ArrayList<>();
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
StandardJavaFileManager fileManager = compiler.getStandardFileManager(
null, null, null);
StandardLocation location = StandardLocation.CLASS_PATH;
String packageName = "commands";
Set<JavaFileObject.Kind> kinds = new HashSet<>();
kinds.add(JavaFileObject.Kind.CLASS);
boolean recurse = false;
Iterable<JavaFileObject> list = fileManager.list(location, packageName,
kinds, recurse);
for (JavaFileObject classFile : list) {
String name = classFile.getName().replaceAll(".*/|[.]class.*","");
commands.add(Class.forName(packageName + "." + name));
}
Works for all packages and classes on the class path, packaged in jar files or without. For classes not explicitly added to the class path, i.e. those loaded by the bootstrap class loader, try setting location to PLATFORM_CLASS_PATH instead.
Here is an utility method, using Spring.
Details about the pattern can be found here
public static List<Class> listMatchingClasses(String matchPattern) throws IOException {
List<Class> classes = new LinkedList<Class>();
PathMatchingResourcePatternResolver scanner = new PathMatchingResourcePatternResolver();
Resource[] resources = scanner.getResources(matchPattern);
for (Resource resource : resources) {
Class<?> clazz = getClassFromResource(resource);
classes.add(clazz);
}
return classes;
}
public static Class getClassFromResource(Resource resource) {
try {
String resourceUri = resource.getURI().toString();
resourceUri = resourceUri.replace(esourceUri.indexOf(".class"), "").replace("/", ".");
// try printing the resourceUri before calling forName, to see if it is OK.
return Class.forName(resourceUri);
} catch (Exception ex) {
ex.printStackTrace();
}
return null;
}
If you do not want to use external depencies and you want to work on your IDE / on a JAR file, you can try this:
public static List<Class<?>> getClassesForPackage(final String pkgName) throws IOException, URISyntaxException {
final String pkgPath = pkgName.replace('.', '/');
final URI pkg = Objects.requireNonNull(ClassLoader.getSystemClassLoader().getResource(pkgPath)).toURI();
final ArrayList<Class<?>> allClasses = new ArrayList<Class<?>>();
Path root;
if (pkg.toString().startsWith("jar:")) {
try {
root = FileSystems.getFileSystem(pkg).getPath(pkgPath);
} catch (final FileSystemNotFoundException e) {
root = FileSystems.newFileSystem(pkg, Collections.emptyMap()).getPath(pkgPath);
}
} else {
root = Paths.get(pkg);
}
final String extension = ".class";
try (final Stream<Path> allPaths = Files.walk(root)) {
allPaths.filter(Files::isRegularFile).forEach(file -> {
try {
final String path = file.toString().replace('/', '.');
final String name = path.substring(path.indexOf(pkgName), path.length() - extension.length());
allClasses.add(Class.forName(name));
} catch (final ClassNotFoundException | StringIndexOutOfBoundsException ignored) {
}
});
}
return allClasses;
}
From: Can you find all classes in a package using reflection?
Start with public Classloader.getResources(String name). Ask the classloader for a class corresponding to each name in the package you are interested. Repeat for all classloaders of relevance.
Yes but its not the easiest thing to do. There are lots of issues with this. Not all of the classes are easy to find. Some classes could be in a: Jar, as a class file, over the network etc.
Take a look at this thread.
To make sure they were the ICommand type then you would have to use reflection to check for the inheriting class.
This would be a very useful tool we need, and JDK should provide some support.
But it's probably better done during build. You know where all your class files are and you can inspect them statically and build a graph. At runtime you can query this graph to get all subtypes. This requires more work, but I believe it really belongs to the build process.
Using Johannes Link's ClasspathSuite, I was able to do it like this:
import org.junit.extensions.cpsuite.ClassTester;
import org.junit.extensions.cpsuite.ClasspathClassesFinder;
public static List<Class<?>> getClasses(final Package pkg, final boolean includeChildPackages) {
return new ClasspathClassesFinder(new ClassTester() {
#Override public boolean searchInJars() { return true; }
#Override public boolean acceptInnerClass() { return false; }
#Override public boolean acceptClassName(String name) {
return name.startsWith(pkg.getName()) && (includeChildPackages || name.indexOf(".", pkg.getName().length()) != -1);
}
#Override public boolean acceptClass(Class<?> c) { return true; }
}, System.getProperty("java.class.path")).find();
}
The ClasspathClassesFinder looks for class files and jars in the system classpath.
In your specific case, you could modify acceptClass like this:
#Override public boolean acceptClass(Class<?> c) {
return ICommand.class.isAssignableFrom(c);
}
One thing to note: be careful what you return in acceptClassName, as the next thing ClasspathClassesFinder does is to load the class and call acceptClass. If acceptClassName always return true, you'll end up loading every class in the classpath and that may cause an OutOfMemoryError.
You could use OpenPojo and do this:
final List<PojoClass> pojoClasses = PojoClassFactory.getPojoClassesRecursively("my.package.path", null);
Then you can go over the list and perform any functionality you desire.

Categories

Resources