diff --git a/api/pom.xml b/api/pom.xml
index a502e8c3..6e8e84f5 100644
--- a/api/pom.xml
+++ b/api/pom.xml
@@ -49,6 +49,27 @@
${netty.version}
compile
+
+ org.apache.maven
+ maven-resolver-provider
+ 3.8.1
+
+ provided
+
+
+ org.apache.maven.resolver
+ maven-resolver-connector-basic
+ 1.6.2
+
+ provided
+
+
+ org.apache.maven.resolver
+ maven-resolver-transport-http
+ 1.6.2
+
+ provided
+
org.yaml
snakeyaml
diff --git a/api/src/main/java/net/md_5/bungee/api/plugin/LibraryLoader.java b/api/src/main/java/net/md_5/bungee/api/plugin/LibraryLoader.java
new file mode 100644
index 00000000..48c31996
--- /dev/null
+++ b/api/src/main/java/net/md_5/bungee/api/plugin/LibraryLoader.java
@@ -0,0 +1,123 @@
+package net.md_5.bungee.api.plugin;
+
+import java.io.File;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import org.apache.maven.repository.internal.MavenRepositorySystemUtils;
+import org.eclipse.aether.DefaultRepositorySystemSession;
+import org.eclipse.aether.RepositorySystem;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.artifact.DefaultArtifact;
+import org.eclipse.aether.collection.CollectRequest;
+import org.eclipse.aether.connector.basic.BasicRepositoryConnectorFactory;
+import org.eclipse.aether.graph.Dependency;
+import org.eclipse.aether.impl.DefaultServiceLocator;
+import org.eclipse.aether.repository.LocalRepository;
+import org.eclipse.aether.repository.RemoteRepository;
+import org.eclipse.aether.repository.RepositoryPolicy;
+import org.eclipse.aether.resolution.ArtifactResult;
+import org.eclipse.aether.resolution.DependencyRequest;
+import org.eclipse.aether.resolution.DependencyResolutionException;
+import org.eclipse.aether.resolution.DependencyResult;
+import org.eclipse.aether.spi.connector.RepositoryConnectorFactory;
+import org.eclipse.aether.spi.connector.transport.TransporterFactory;
+import org.eclipse.aether.transfer.AbstractTransferListener;
+import org.eclipse.aether.transfer.TransferCancelledException;
+import org.eclipse.aether.transfer.TransferEvent;
+import org.eclipse.aether.transport.http.HttpTransporterFactory;
+
+class LibraryLoader
+{
+
+ private final Logger logger;
+ private final RepositorySystem repository;
+ private final DefaultRepositorySystemSession session;
+ private final List repositories;
+
+ public LibraryLoader(Logger logger)
+ {
+ this.logger = logger;
+
+ DefaultServiceLocator locator = MavenRepositorySystemUtils.newServiceLocator();
+ locator.addService( RepositoryConnectorFactory.class, BasicRepositoryConnectorFactory.class );
+ locator.addService( TransporterFactory.class, HttpTransporterFactory.class );
+
+ this.repository = locator.getService( RepositorySystem.class );
+ this.session = MavenRepositorySystemUtils.newSession();
+
+ session.setChecksumPolicy( RepositoryPolicy.CHECKSUM_POLICY_FAIL );
+ session.setLocalRepositoryManager( repository.newLocalRepositoryManager( session, new LocalRepository( "libraries" ) ) );
+ session.setTransferListener( new AbstractTransferListener()
+ {
+ @Override
+ public void transferStarted(TransferEvent event) throws TransferCancelledException
+ {
+ logger.log( Level.INFO, "Downloading {0}", event.getResource().getRepositoryUrl() + event.getResource().getResourceName() );
+ }
+ } );
+ session.setReadOnly();
+
+ this.repositories = repository.newResolutionRepositories( session, Arrays.asList( new RemoteRepository.Builder( "central", "default", "https://repo.maven.apache.org/maven2" ).build() ) );
+ }
+
+ public ClassLoader createLoader(PluginDescription desc)
+ {
+ if ( desc.getLibraries().isEmpty() )
+ {
+ return null;
+ }
+ logger.log( Level.INFO, "[{0}] Loading {1} libraries... please wait", new Object[]
+ {
+ desc.getName(), desc.getLibraries().size()
+ } );
+
+ List dependencies = new ArrayList<>();
+ for ( String library : desc.getLibraries() )
+ {
+ Artifact artifact = new DefaultArtifact( library );
+ Dependency dependency = new Dependency( artifact, null );
+
+ dependencies.add( dependency );
+ }
+
+ DependencyResult result;
+ try
+ {
+ result = repository.resolveDependencies( session, new DependencyRequest( new CollectRequest( (Dependency) null, dependencies, repositories ), null ) );
+ } catch ( DependencyResolutionException ex )
+ {
+ throw new RuntimeException( "Error resolving libraries", ex );
+ }
+
+ List jarFiles = new ArrayList<>();
+ for ( ArtifactResult artifact : result.getArtifactResults() )
+ {
+ File file = artifact.getArtifact().getFile();
+
+ URL url;
+ try
+ {
+ url = file.toURI().toURL();
+ } catch ( MalformedURLException ex )
+ {
+ throw new AssertionError( ex );
+ }
+
+ jarFiles.add( url );
+ logger.log( Level.INFO, "[{0}] Loaded library {1}", new Object[]
+ {
+ desc.getName(), file
+ } );
+ }
+
+ URLClassLoader loader = new URLClassLoader( jarFiles.toArray( new URL[ jarFiles.size() ] ) );
+
+ return loader;
+ }
+}
diff --git a/api/src/main/java/net/md_5/bungee/api/plugin/PluginClassloader.java b/api/src/main/java/net/md_5/bungee/api/plugin/PluginClassloader.java
index 75c75e2d..cda92c9c 100644
--- a/api/src/main/java/net/md_5/bungee/api/plugin/PluginClassloader.java
+++ b/api/src/main/java/net/md_5/bungee/api/plugin/PluginClassloader.java
@@ -1,12 +1,23 @@
package net.md_5.bungee.api.plugin;
import com.google.common.base.Preconditions;
+import com.google.common.io.ByteStreams;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
import java.net.URL;
import java.net.URLClassLoader;
+import java.security.CodeSigner;
+import java.security.CodeSource;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
+import java.util.jar.Manifest;
+import lombok.ToString;
import net.md_5.bungee.api.ProxyServer;
+@ToString(of = "desc")
final class PluginClassloader extends URLClassLoader
{
@@ -14,6 +25,10 @@ final class PluginClassloader extends URLClassLoader
//
private final ProxyServer proxy;
private final PluginDescription desc;
+ private final JarFile jar;
+ private final Manifest manifest;
+ private final URL url;
+ private final ClassLoader libraryLoader;
//
private Plugin plugin;
@@ -22,11 +37,18 @@ final class PluginClassloader extends URLClassLoader
ClassLoader.registerAsParallelCapable();
}
- public PluginClassloader(ProxyServer proxy, PluginDescription desc, URL[] urls)
+ public PluginClassloader(ProxyServer proxy, PluginDescription desc, File file, ClassLoader libraryLoader) throws IOException
{
- super( urls );
+ super( new URL[]
+ {
+ file.toURI().toURL()
+ } );
this.proxy = proxy;
this.desc = desc;
+ this.jar = new JarFile( file );
+ this.manifest = jar.getManifest();
+ this.url = file.toURI().toURL();
+ this.libraryLoader = libraryLoader;
allLoaders.add( this );
}
@@ -34,10 +56,10 @@ final class PluginClassloader extends URLClassLoader
@Override
protected Class> loadClass(String name, boolean resolve) throws ClassNotFoundException
{
- return loadClass0( name, resolve, true );
+ return loadClass0( name, resolve, true, true );
}
- private Class> loadClass0(String name, boolean resolve, boolean checkOther) throws ClassNotFoundException
+ private Class> loadClass0(String name, boolean resolve, boolean checkOther, boolean checkLibraries) throws ClassNotFoundException
{
try
{
@@ -45,6 +67,17 @@ final class PluginClassloader extends URLClassLoader
} catch ( ClassNotFoundException ex )
{
}
+
+ if ( checkLibraries && libraryLoader != null )
+ {
+ try
+ {
+ return libraryLoader.loadClass( name );
+ } catch ( ClassNotFoundException ex )
+ {
+ }
+ }
+
if ( checkOther )
{
for ( PluginClassloader loader : allLoaders )
@@ -53,13 +86,66 @@ final class PluginClassloader extends URLClassLoader
{
try
{
- return loader.loadClass0( name, resolve, false );
+ return loader.loadClass0( name, resolve, false, proxy.getPluginManager().isTransitiveDepend( desc, loader.desc ) );
} catch ( ClassNotFoundException ex )
{
}
}
}
}
+
+ throw new ClassNotFoundException( name );
+ }
+
+ @Override
+ protected Class> findClass(String name) throws ClassNotFoundException
+ {
+ String path = name.replace( '.', '/' ).concat( ".class" );
+ JarEntry entry = jar.getJarEntry( path );
+
+ if ( entry != null )
+ {
+ byte[] classBytes;
+
+ try ( InputStream is = jar.getInputStream( entry ) )
+ {
+ classBytes = ByteStreams.toByteArray( is );
+ } catch ( IOException ex )
+ {
+ throw new ClassNotFoundException( name, ex );
+ }
+
+ int dot = name.lastIndexOf( '.' );
+ if ( dot != -1 )
+ {
+ String pkgName = name.substring( 0, dot );
+ if ( getPackage( pkgName ) == null )
+ {
+ try
+ {
+ if ( manifest != null )
+ {
+ definePackage( pkgName, manifest, url );
+ } else
+ {
+ definePackage( pkgName, null, null, null, null, null, null, null );
+ }
+ } catch ( IllegalArgumentException ex )
+ {
+ if ( getPackage( pkgName ) == null )
+ {
+ throw new IllegalStateException( "Cannot find package " + pkgName );
+ }
+ }
+ }
+ }
+
+ CodeSigner[] signers = entry.getCodeSigners();
+ CodeSource source = new CodeSource( url, signers );
+
+ return defineClass( name, classBytes, 0, classBytes.length, source );
+ }
+
throw new ClassNotFoundException( name );
}
diff --git a/api/src/main/java/net/md_5/bungee/api/plugin/PluginDescription.java b/api/src/main/java/net/md_5/bungee/api/plugin/PluginDescription.java
index ef12ae90..ea5c0d37 100644
--- a/api/src/main/java/net/md_5/bungee/api/plugin/PluginDescription.java
+++ b/api/src/main/java/net/md_5/bungee/api/plugin/PluginDescription.java
@@ -2,6 +2,8 @@ package net.md_5.bungee.api.plugin;
import java.io.File;
import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
import java.util.Set;
import lombok.AllArgsConstructor;
import lombok.Data;
@@ -48,4 +50,8 @@ public class PluginDescription
* Optional description.
*/
private String description = null;
+ /**
+ * Optional libraries.
+ */
+ private List libraries = new LinkedList<>();
}
diff --git a/api/src/main/java/net/md_5/bungee/api/plugin/PluginManager.java b/api/src/main/java/net/md_5/bungee/api/plugin/PluginManager.java
index b4da08dc..c122cad9 100644
--- a/api/src/main/java/net/md_5/bungee/api/plugin/PluginManager.java
+++ b/api/src/main/java/net/md_5/bungee/api/plugin/PluginManager.java
@@ -4,10 +4,12 @@ import com.google.common.base.Preconditions;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Multimap;
import com.google.common.eventbus.Subscribe;
+import com.google.common.graph.GraphBuilder;
+import com.google.common.graph.Graphs;
+import com.google.common.graph.MutableGraph;
import java.io.File;
import java.io.InputStream;
import java.lang.reflect.Method;
-import java.net.URL;
import java.net.URLClassLoader;
import java.util.Arrays;
import java.util.Collection;
@@ -49,6 +51,8 @@ public final class PluginManager
private final Yaml yaml;
private final EventBus eventBus;
private final Map plugins = new LinkedHashMap<>();
+ private final MutableGraph dependencyGraph = GraphBuilder.directed().build();
+ private final LibraryLoader libraryLoader;
private final Map commandMap = new HashMap<>();
private Map toLoad = new HashMap<>();
private final Multimap commandsByPlugin = ArrayListMultimap.create();
@@ -67,6 +71,17 @@ public final class PluginManager
yaml = new Yaml( yamlConstructor );
eventBus = new EventBus( proxy.getLogger() );
+
+ LibraryLoader libraryLoader = null;
+ try
+ {
+ libraryLoader = new LibraryLoader( proxy.getLogger() );
+ } catch ( NoClassDefFoundError ex )
+ {
+ // Provided depends were not added back
+ proxy.getLogger().warning( "Could not initialize LibraryLoader (missing dependencies?)" );
+ }
+ this.libraryLoader = libraryLoader;
}
/**
@@ -309,6 +324,7 @@ public final class PluginManager
status = false;
}
+ dependencyGraph.putEdge( plugin.getName(), dependName );
if ( !status )
{
break;
@@ -320,10 +336,7 @@ public final class PluginManager
{
try
{
- URLClassLoader loader = new PluginClassloader( proxy, plugin, new URL[]
- {
- plugin.getFile().toURI().toURL()
- } );
+ URLClassLoader loader = new PluginClassloader( proxy, plugin, plugin.getFile(), ( libraryLoader != null ) ? libraryLoader.createLoader( plugin ) : null );
Class> main = loader.loadClass( plugin.getMain() );
Plugin clazz = (Plugin) main.getDeclaredConstructor().newInstance();
@@ -335,7 +348,7 @@ public final class PluginManager
} );
} catch ( Throwable t )
{
- proxy.getLogger().log( Level.WARNING, "Error enabling plugin " + plugin.getName(), t );
+ proxy.getLogger().log( Level.WARNING, "Error loading plugin " + plugin.getName(), t );
}
}
@@ -463,4 +476,19 @@ public final class PluginManager
{
return Collections.unmodifiableCollection( commandMap.entrySet() );
}
+
+ boolean isTransitiveDepend(PluginDescription plugin, PluginDescription depend)
+ {
+ Preconditions.checkArgument( plugin != null, "plugin" );
+ Preconditions.checkArgument( depend != null, "depend" );
+
+ if ( dependencyGraph.nodes().contains( plugin.getName() ) )
+ {
+ if ( Graphs.reachableNodes( dependencyGraph, plugin.getName() ).contains( depend.getName() ) )
+ {
+ return true;
+ }
+ }
+ return false;
+ }
}
diff --git a/proxy/pom.xml b/proxy/pom.xml
index 9f663469..61fb794d 100644
--- a/proxy/pom.xml
+++ b/proxy/pom.xml
@@ -91,6 +91,31 @@
5.1.49
runtime
+
+
+ org.apache.maven
+ maven-resolver-provider
+ 3.8.1
+ compile
+
+
+ org.apache.maven.resolver
+ maven-resolver-connector-basic
+ 1.6.2
+ compile
+
+
+ org.apache.maven.resolver
+ maven-resolver-transport-http
+ 1.6.2
+ compile
+
+
+ org.slf4j
+ slf4j-jdk14
+ 1.7.30
+ compile
+