├── .gitignore ├── README.md ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src └── main └── java └── pluginlib ├── DependentJavaPlugin.java ├── FileRelocator.java ├── PluginLib.java └── Relocation.java /.gitignore: -------------------------------------------------------------------------------- 1 | # User-specific stuff 2 | .idea/ 3 | 4 | *.iml 5 | *.ipr 6 | *.iws 7 | 8 | # IntelliJ 9 | out/ 10 | # mpeltonen/sbt-idea plugin 11 | .idea_modules/ 12 | 13 | # JIRA plugin 14 | atlassian-ide-plugin.xml 15 | 16 | # Compiled class file 17 | *.class 18 | 19 | # Log file 20 | *.log 21 | 22 | # BlueJ files 23 | *.ctxt 24 | 25 | # Package Files # 26 | *.jar 27 | *.war 28 | *.nar 29 | *.ear 30 | *.zip 31 | *.tar.gz 32 | *.rar 33 | 34 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 35 | hs_err_pid* 36 | 37 | *~ 38 | 39 | # temporary files which can be created if a process still has a handle open of a deleted file 40 | .fuse_hidden* 41 | 42 | # KDE directory preferences 43 | .directory 44 | 45 | # Linux trash folder which might appear on any partition or disk 46 | .Trash-* 47 | 48 | # .nfs files are created when an open file is removed but is still being accessed 49 | .nfs* 50 | 51 | # General 52 | .DS_Store 53 | .AppleDouble 54 | .LSOverride 55 | 56 | # Icon must end with two \r 57 | Icon 58 | 59 | # Thumbnails 60 | ._* 61 | 62 | # Files that might appear in the root of a volume 63 | .DocumentRevisions-V100 64 | .fseventsd 65 | .Spotlight-V100 66 | .TemporaryItems 67 | .Trashes 68 | .VolumeIcon.icns 69 | .com.apple.timemachine.donotpresent 70 | 71 | # Directories potentially created on remote AFP share 72 | .AppleDB 73 | .AppleDesktop 74 | Network Trash Folder 75 | Temporary Items 76 | .apdisk 77 | 78 | # Windows thumbnail cache files 79 | Thumbs.db 80 | Thumbs.db:encryptable 81 | ehthumbs.db 82 | ehthumbs_vista.db 83 | 84 | # Dump file 85 | *.stackdump 86 | 87 | # Folder config file 88 | [Dd]esktop.ini 89 | 90 | # Recycle Bin used on file shares 91 | $RECYCLE.BIN/ 92 | 93 | # Windows Installer files 94 | *.cab 95 | *.msi 96 | *.msix 97 | *.msm 98 | *.msp 99 | 100 | # Windows shortcuts 101 | *.lnk 102 | 103 | .gradle 104 | build/ 105 | 106 | # Ignore Gradle GUI config 107 | gradle-app.setting 108 | 109 | # Cache of project 110 | .gradletasknamecache 111 | 112 | **/build/ 113 | 114 | # Common working directory 115 | run/ 116 | examples/ 117 | 118 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 119 | !gradle-wrapper.jar 120 | copier.bat 121 | examples/maven/.gitignore 122 | examples/maven/pom.xml 123 | examples/maven/src/ 124 | src/main/java/org/ 125 | src/main/resources/ 126 | src/test/ 127 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # PluginLib 3 | A simple utility to allow downloading libraries and relocating them at runtime without having to shade them 4 | 5 | # Usage 6 | Simply extend **pluginlib.DependentJavaPlugin** instead of Bukkit's *JavaPlugin*. 7 | ```java 8 | package org.example; 9 | 10 | import pluginlib.DependentJavaPlugin; 11 | 12 | public class ExamplePlugin extends DependentJavaPlugin { 13 | 14 | } 15 | ``` 16 | 17 | To add libraries, you have 2 ways: 18 | 19 | ## Through YML (Preferable) 20 | Define them in your **plugin.yml** through the `runtime-libraries` section: 21 | ```yml 22 | name: ExamplePlugin 23 | main: org.example.ExamplePlugin 24 | version: 1.0-SNAPSHOT 25 | authors: [...] 26 | runtime-libraries: 27 | relocation-prefix: 'org.example.libs' # Required if we want to relocate libraries 28 | libraries-folder: 'libraries' # If not defined, it will be 'libs' 29 | libraries: 30 | kotlin: 31 | groupId: org.jetbrains.kotlin 32 | artifactId: kotlin-stdlib 33 | version: 1.4.20 34 | ``` 35 | Here we want to get **Kotlin**'s standard library. 36 | Unless explicitly specified, **dependencies will be downloaded from Maven Central**. We can specify the repository we want to get from, using the `repository` property: 37 | ```yml 38 | jda: 39 | groupId: net.dv8tion 40 | artifactId: JDA 41 | version: 4.2.0_222 42 | repository: https://jcenter.bintray.com/ 43 | ``` 44 | This way, JDA will be downloaded from Bintray JCenter's repository. 45 | 46 | There are multiple ways to define a library, aside from defining the library properties (group id, artifact id, etc) 47 | 48 | ### Through Maven-like XML 49 | For the sake of productivity, and because we love being lazy, you can **straight define the library's XML, much similar to Maven's format**: 50 | ```yml 51 | kotlin: 52 | xml: ' 53 | 54 | org.jetbrains.kotlin 55 | kotlin-stdlib 56 | 1.4.20 57 | 58 | ' 59 | ``` 60 | This can be especially helpful if you don't want to bother converting the XML to YML, and if you just copied it straight from a website. **Warning: By time, your plugin.yml may get a little unclean if you keep doing this!** 61 | 62 | ### Defining a URL directly 63 | Some libraries (although rare), do not follow the standard `repository:/group/artifact/version/artifact-version.jar` format, and may require us to explicitly define the URL we want to download the library from. 64 | 65 | Let's imagine we want to use **[Aikar's Minecraft Timings](https://github.com/aikar/minecraft-timings)**: 66 | ```yml 67 | timings: 68 | url: 'http://repo.aikar.co/nexus/content/groups/aikar/co/aikar/minecraft-timings/1.0.4/minecraft-timings-1.0.4.jar' 69 | artifactId: 'mc-timings' 70 | version: 1.0.4 71 | ``` 72 | **Important note**: When we follow this format, we **must** define the artifact ID and the version. 73 | 74 | This can also be useful if what we're looking for is to download a JAR, rather than an actual library. 75 | 76 | **Defining in your plugin.yml has a major advantage**: When your plugin loads, libraries will already have been loaded beforehand. 77 | **This means you can use your library components anywhere in your code, such as static blocks and initializers.** 78 | 79 | ## Load libraries using code 80 | You can also load libraries programmatically, instead of defining them in your plugin.yml. 81 | 82 | This has a few advantages, such as being able to load your library conditionally (for example, we might need to load MySQL drivers only when the chosen database type is MySQL in our config), however it also has the disadvantage of being unable to use your library until it is explicitly loaded. 83 | 84 | For example, if we want to use **[Caffeine](https://github.com/ben-manes/caffeine)**: 85 | ```java 86 | package org.example; 87 | 88 | import pluginlib.DependentJavaPlugin; 89 | import pluginlib.PluginLib; 90 | 91 | public class ExamplePlugin extends DependentJavaPlugin { 92 | 93 | private static final PluginLib CAFFEINE = PluginLib.builder() 94 | .groupId("com.github.ben-manes.caffeine") 95 | .artifactId("caffeine") 96 | .version("2.8.6") 97 | .build(); 98 | 99 | static { 100 | CAFFEINE.load(ExamplePlugin.class); 101 | } 102 | 103 | } 104 | ``` 105 | 106 | Just like the plugin.yml, we can also parse from XML: 107 | ```java 108 | private static final PluginLib CAFFEINE = PluginLib.parseXML( 109 | "" + 110 | " com.github.ben-manes.caffeine" + 111 | " caffeine" + 112 | " 2.8.6" + 113 | "" 114 | ).build(); 115 | ``` 116 | 117 | And, we can also parse the library from a URL: 118 | ```java 119 | private static final PluginLib TIMINGS = PluginLib 120 | .fromURL("https://repo.aikar.co/nexus/content/groups/aikar/co/aikar/minecraft-timings/1.0.4/minecraft-timings-1.0.4.jar") 121 | .artifactId("timings") 122 | .version("1.0.4") 123 | .build(); 124 | 125 | static { 126 | TIMINGS.load(ExamplePlugin.class); 127 | } 128 | ``` 129 | 130 | # Relocating 131 | Relocation is some re-mapping your code and dependencies to make them unique and not conflict with other plugins or dependencies. For example, if we want to relocate `okhttp3`'s library, its paths would change: 132 | `okhttp3.OkHttpClient` -> `org.example.okhttp3.OkHttpClient`. 133 | And the same with all other classes that fall in `okhttp3`'s package. If any plugin also includes okhttp3, we will be sure that there will not be any conflicts between our plugin and the others. 134 | 135 | ## Through YML 136 | In our library's definition, we simply add the `relocation` section 137 | ```yml 138 | jda: 139 | groupId: net.dv8tion 140 | artifactId: JDA 141 | version: 4.2.0_222 142 | repository: https://jcenter.bintray.com/ 143 | relocation: 144 | net.dv8tion.jda: jda 145 | ``` 146 | Relocation follows the following template: 147 | `: 'path to replace'` 148 | library name would be `jda`, and in relocation it would appear as **\.\**, as in, **org.example.libs.jda**. 149 | 150 | ## Through code 151 | We can specify relocation rules through our code in the builder: 152 | ```java 153 | package org.example; 154 | 155 | import pluginlib.DependentJavaPlugin; 156 | import pluginlib.PluginLib; 157 | import pluginlib.Relocation; 158 | 159 | public class ExamplePlugin extends DependentJavaPlugin { 160 | 161 | private static final PluginLib CAFFEINE = PluginLib.builder() 162 | .groupId("com.github.ben-manes.caffeine") 163 | .artifactId("caffeine") 164 | .version("2.8.6") 165 | .relocate(new Relocation("com/github/benmanes/caffeine", "org.example.libs.caffeine")) 166 | .build(); 167 | 168 | static { 169 | CAFFEINE.load(ExamplePlugin.class); 170 | } 171 | } 172 | ``` 173 | 174 | 175 | **Very important note**: Relocating using plugin.yml or code **is not enough!** You must tell your build system (Maven or Gradle) to relocate references in your code as well. 176 | 177 | * Relocating in **[Maven](https://maven.apache.org/plugins/maven-shade-plugin/examples/class-relocation.html)** 178 | * Relocating in **[Gradle](https://imperceptiblethoughts.com/shadow/configuration/relocation/)** 179 | 180 | Notice that we did not directly put `com.github.benmanes`, and instead `com/github/benmanes`. This way, we will be able to outsmart build system relocation, and will not have this very string literal relocated. 181 | 182 | # Full examples 183 | You can view full examples in **[maven](https://github.com/ReflxctionDev/PluginLib/tree/maven)** and **[gradle](https://github.com/ReflxctionDev/PluginLib/tree/gradle)** branches. 184 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | import org.apache.tools.ant.filters.ReplaceTokens 2 | 3 | apply plugin: 'java' 4 | 5 | group = 'pluginlib' 6 | version = '1.3' 7 | 8 | sourceCompatibility = targetCompatibility = '1.8' 9 | 10 | repositories { 11 | mavenCentral() 12 | maven { url = 'https://hub.spigotmc.org/nexus/content/repositories/snapshots/' } 13 | maven { url 'https://oss.sonatype.org/content/groups/public/' } 14 | } 15 | 16 | dependencies { 17 | compileOnly 'org.spigotmc:spigot-api:1.8.8-R0.1-SNAPSHOT' 18 | compileOnly 'org.jetbrains:annotations:20.1.0' 19 | } 20 | 21 | processResources { 22 | from(sourceSets.main.resources.srcDirs) { 23 | filter ReplaceTokens, tokens: [version: version] 24 | } 25 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Revxrsal/PluginLib/94f07e378895e6fe5898f110193256bbe97581a2/gradle.properties -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Revxrsal/PluginLib/94f07e378895e6fe5898f110193256bbe97581a2/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.1-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | # Determine the Java command to use to start the JVM. 86 | if [ -n "$JAVA_HOME" ] ; then 87 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 88 | # IBM's JDK on AIX uses strange locations for the executables 89 | JAVACMD="$JAVA_HOME/jre/sh/java" 90 | else 91 | JAVACMD="$JAVA_HOME/bin/java" 92 | fi 93 | if [ ! -x "$JAVACMD" ] ; then 94 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 95 | 96 | Please set the JAVA_HOME variable in your environment to match the 97 | location of your Java installation." 98 | fi 99 | else 100 | JAVACMD="java" 101 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 102 | 103 | Please set the JAVA_HOME variable in your environment to match the 104 | location of your Java installation." 105 | fi 106 | 107 | # Increase the maximum file descriptors if we can. 108 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 109 | MAX_FD_LIMIT=`ulimit -H -n` 110 | if [ $? -eq 0 ] ; then 111 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 112 | MAX_FD="$MAX_FD_LIMIT" 113 | fi 114 | ulimit -n $MAX_FD 115 | if [ $? -ne 0 ] ; then 116 | warn "Could not set maximum file descriptor limit: $MAX_FD" 117 | fi 118 | else 119 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 120 | fi 121 | fi 122 | 123 | # For Darwin, add options to specify how the application appears in the dock 124 | if $darwin; then 125 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 126 | fi 127 | 128 | # For Cygwin or MSYS, switch paths to Windows format before running java 129 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 130 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 131 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 132 | JAVACMD=`cygpath --unix "$JAVACMD"` 133 | 134 | # We build the pattern for arguments to be converted via cygpath 135 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 136 | SEP="" 137 | for dir in $ROOTDIRSRAW ; do 138 | ROOTDIRS="$ROOTDIRS$SEP$dir" 139 | SEP="|" 140 | done 141 | OURCYGPATTERN="(^($ROOTDIRS))" 142 | # Add a user-defined pattern to the cygpath arguments 143 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 144 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 145 | fi 146 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 147 | i=0 148 | for arg in "$@" ; do 149 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 150 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 151 | 152 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 153 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 154 | else 155 | eval `echo args$i`="\"$arg\"" 156 | fi 157 | i=$((i+1)) 158 | done 159 | case $i in 160 | (0) set -- ;; 161 | (1) set -- "$args0" ;; 162 | (2) set -- "$args0" "$args1" ;; 163 | (3) set -- "$args0" "$args1" "$args2" ;; 164 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 165 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 166 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 167 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 168 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 169 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 170 | esac 171 | fi 172 | 173 | # Escape application args 174 | save () { 175 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 176 | echo " " 177 | } 178 | APP_ARGS=$(save "$@") 179 | 180 | # Collect all arguments for the java command, following the shell quoting and substitution rules 181 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 182 | 183 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 184 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 185 | cd "$(dirname "$0")" 186 | fi 187 | 188 | exec "$JAVACMD" "$@" 189 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 33 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 34 | 35 | @rem Find java.exe 36 | if defined JAVA_HOME goto findJavaFromJavaHome 37 | 38 | set JAVA_EXE=java.exe 39 | %JAVA_EXE% -version >NUL 2>&1 40 | if "%ERRORLEVEL%" == "0" goto init 41 | 42 | echo. 43 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 44 | echo. 45 | echo Please set the JAVA_HOME variable in your environment to match the 46 | echo location of your Java installation. 47 | 48 | goto fail 49 | 50 | :findJavaFromJavaHome 51 | set JAVA_HOME=%JAVA_HOME:"=% 52 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 53 | 54 | if exist "%JAVA_EXE%" goto init 55 | 56 | echo. 57 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 58 | echo. 59 | echo Please set the JAVA_HOME variable in your environment to match the 60 | echo location of your Java installation. 61 | 62 | goto fail 63 | 64 | :init 65 | @rem Get command-line arguments, handling Windows variants 66 | 67 | if not "%OS%" == "Windows_NT" goto win9xME_args 68 | 69 | :win9xME_args 70 | @rem Slurp the command line arguments. 71 | set CMD_LINE_ARGS= 72 | set _SKIP=2 73 | 74 | :win9xME_args_slurp 75 | if "x%~1" == "x" goto execute 76 | 77 | set CMD_LINE_ARGS=%* 78 | 79 | :execute 80 | @rem Setup the command line 81 | 82 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 83 | 84 | @rem Execute Gradle 85 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 86 | 87 | :end 88 | @rem End local scope for the variables with windows NT shell 89 | if "%ERRORLEVEL%"=="0" goto mainEnd 90 | 91 | :fail 92 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 93 | rem the _cmd.exe /c_ return code! 94 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 95 | exit /b 1 96 | 97 | :mainEnd 98 | if "%OS%"=="Windows_NT" endlocal 99 | 100 | :omega 101 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'PluginLib' 2 | -------------------------------------------------------------------------------- /src/main/java/pluginlib/DependentJavaPlugin.java: -------------------------------------------------------------------------------- 1 | package pluginlib; 2 | 3 | import org.bukkit.plugin.java.JavaPlugin; 4 | 5 | /** 6 | * Represents an extension of {@link JavaPlugin} that allows you to download and relocate libraries 7 | * at runtime. 8 | * 9 | * For information on using, please check this guide. 10 | */ 11 | public abstract class DependentJavaPlugin extends JavaPlugin { 12 | 13 | static { 14 | FileRelocator.load(DependentJavaPlugin.class); 15 | PluginLib.loadLibs(); 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/pluginlib/FileRelocator.java: -------------------------------------------------------------------------------- 1 | package pluginlib; 2 | 3 | import java.io.File; 4 | import java.lang.reflect.Constructor; 5 | import java.lang.reflect.Method; 6 | import java.util.HashMap; 7 | import java.util.Map; 8 | import java.util.Set; 9 | 10 | /** 11 | * Utility class that handles relocation 12 | */ 13 | public abstract class FileRelocator { 14 | 15 | private static final PluginLib asm = PluginLib.builder() 16 | .groupId("org.ow2.asm") 17 | .artifactId("asm") 18 | .version("7.1") 19 | .build(); 20 | 21 | private static final PluginLib asm_commons = PluginLib.builder() 22 | .groupId("org.ow2.asm") 23 | .artifactId("asm-commons") 24 | .version("7.1") 25 | .build(); 26 | 27 | private static final PluginLib jarRelocator = PluginLib.builder() 28 | .groupId("me.lucko") 29 | .artifactId("jar-relocator") 30 | .version("1.4") 31 | .build(); 32 | 33 | private static Constructor relocatorConstructor; 34 | private static Method relocateMethod; 35 | 36 | /** 37 | * Loads all the required libraries for relocation 38 | * 39 | * @param clazz Class that extends {@link DependentJavaPlugin}. 40 | */ 41 | public static void load(Class clazz) { 42 | asm.load(clazz); 43 | asm_commons.load(clazz); 44 | jarRelocator.load(clazz); 45 | 46 | try { 47 | Class reloc = Class.forName("me.lucko.jarrelocator.JarRelocator"); 48 | relocatorConstructor = reloc.getDeclaredConstructor(File.class, File.class, Map.class); 49 | relocatorConstructor.setAccessible(true); 50 | relocateMethod = reloc.getDeclaredMethod("run"); 51 | relocateMethod.setAccessible(true); 52 | } catch (ClassNotFoundException | NoSuchMethodException e) { 53 | e.printStackTrace(); 54 | } 55 | } 56 | 57 | public static void remap(File input, File output, Set relocations) throws Exception { 58 | Map mappings = new HashMap<>(); 59 | for (Relocation relocation : relocations) { 60 | mappings.put(relocation.getPath(), relocation.getNewPath()); 61 | } 62 | 63 | // create and invoke a new relocator 64 | Object relocator = relocatorConstructor.newInstance(input, output, mappings); 65 | relocateMethod.invoke(relocator); 66 | } 67 | 68 | private FileRelocator() { 69 | throw new AssertionError("Cannot create instances of " + getClass().getName() + "."); 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /src/main/java/pluginlib/PluginLib.java: -------------------------------------------------------------------------------- 1 | package pluginlib; 2 | 3 | import org.bukkit.Bukkit; 4 | import org.intellij.lang.annotations.Language; 5 | import org.jetbrains.annotations.NotNull; 6 | import org.w3c.dom.Document; 7 | import org.xml.sax.InputSource; 8 | import org.xml.sax.SAXException; 9 | import org.yaml.snakeyaml.Yaml; 10 | 11 | import javax.xml.parsers.DocumentBuilder; 12 | import javax.xml.parsers.DocumentBuilderFactory; 13 | import javax.xml.parsers.ParserConfigurationException; 14 | import java.io.*; 15 | import java.lang.reflect.Method; 16 | import java.net.MalformedURLException; 17 | import java.net.URL; 18 | import java.net.URLClassLoader; 19 | import java.nio.file.Files; 20 | import java.util.*; 21 | import java.util.Map.Entry; 22 | import java.util.function.Supplier; 23 | 24 | import static java.nio.file.StandardCopyOption.REPLACE_EXISTING; 25 | import static java.util.Objects.requireNonNull; 26 | 27 | /** 28 | * Represents a runtime-downloaded plugin library. 29 | *

30 | * This class is immutable, hence is thread-safe. However, certain methods like {@link #load(Class)} are 31 | * most likely not thread-safe. 32 | */ 33 | public class PluginLib { 34 | 35 | public final String groupId, artifactId, version, repository; 36 | public final Set relocationRules; 37 | private final boolean hasRelocations; 38 | 39 | public PluginLib(String groupId, String artifactId, String version, String repository, Set relocationRules) { 40 | this.groupId = groupId; 41 | this.artifactId = artifactId; 42 | this.version = version; 43 | this.repository = repository; 44 | this.relocationRules = Collections.unmodifiableSet(relocationRules); 45 | hasRelocations = !relocationRules.isEmpty(); 46 | } 47 | 48 | /** 49 | * Creates a standard builder 50 | * 51 | * @return The newly created builder. 52 | */ 53 | public static Builder builder() { 54 | return new Builder(); 55 | } 56 | 57 | /** 58 | * Returns a new {@link Builder} that downloads its dependency from a URL. 59 | * 60 | * @param url URL to download 61 | * @return The newly created builder 62 | */ 63 | public static Builder fromURL(@NotNull String url) { 64 | return new Builder().fromURL(url); 65 | } 66 | 67 | /** 68 | * Returns a new {@link Builder} 69 | * 70 | * @param xml XML to parse. Must be exactly like the one in maven. 71 | * @return A new {@link Builder} instance, derived from the XML. 72 | * @throws IllegalArgumentException If the specified XML cannot be parsed. 73 | */ 74 | public static Builder parseXML(@Language("XML") @NotNull String xml) { 75 | try { 76 | DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); 77 | InputSource is = new InputSource(new StringReader(xml)); 78 | Document doc = builder.parse(is); 79 | doc.getDocumentElement().normalize(); 80 | return builder() 81 | .groupId(doc.getElementsByTagName("groupId").item(0).getTextContent()) 82 | .artifactId(doc.getElementsByTagName("artifactId").item(0).getTextContent()) 83 | .version(doc.getElementsByTagName("version").item(0).getTextContent()); 84 | } catch (ParserConfigurationException | SAXException | IOException e) { 85 | throw new IllegalArgumentException("Failed to parse XML: " + e.getMessage()); 86 | } 87 | } 88 | 89 | /** 90 | * Loads this library and handles any relocations if any. 91 | * 92 | * @param clazz Class to use its {@link ClassLoader} to load. 93 | */ 94 | public void load(Class clazz) { 95 | LibrariesOptions options = librariesOptions.get(); 96 | if (options == null) return; 97 | String name = artifactId + "-" + version; 98 | File parent = libFile.get(); 99 | File saveLocation = new File(parent, name + ".jar"); 100 | if (hasRelocations) { 101 | File relocated = new File(parent, name + "-relocated.jar"); 102 | if (relocated.exists()) return; 103 | } 104 | if (!saveLocation.exists()) { 105 | try { 106 | URL url = asURL(); 107 | saveLocation.createNewFile(); 108 | try (InputStream is = url.openStream()) { 109 | Files.copy(is, saveLocation.toPath(), REPLACE_EXISTING); 110 | } 111 | } catch (Exception e) { 112 | e.printStackTrace(); 113 | } 114 | } 115 | if (!saveLocation.exists()) { 116 | throw new RuntimeException("Unable to download dependency: " + artifactId); 117 | } 118 | if (hasRelocations) { 119 | File old = saveLocation; 120 | File relocated = new File(parent, name + "-relocated.jar"); 121 | if (!relocated.exists()) { 122 | try { 123 | relocated.createNewFile(); 124 | FileRelocator.remap(saveLocation, new File(parent, name + "-relocated.jar"), relocationRules); 125 | } catch (Exception e) { 126 | e.printStackTrace(); 127 | } finally { 128 | if (options.deleteAfterRelocation) 129 | old.delete(); 130 | } 131 | } 132 | saveLocation = relocated; 133 | } 134 | 135 | try { 136 | URLClassLoader classLoader = (URLClassLoader) clazz.getClassLoader(); 137 | addURL.invoke(classLoader, saveLocation.toURI().toURL()); 138 | } catch (Exception e) { 139 | throw new RuntimeException("Unable to load dependency: " + saveLocation.toString(), e); 140 | } 141 | } 142 | 143 | /** 144 | * Creates a download {@link URL} for this library. 145 | * 146 | * @return The dependency URL 147 | * @throws MalformedURLException If the URL is malformed. 148 | */ 149 | public URL asURL() throws MalformedURLException { 150 | String repo = repository; 151 | if (!repo.endsWith("/")) { 152 | repo += "/"; 153 | } 154 | repo += "%s/%s/%s/%s-%s.jar"; 155 | 156 | String url = String.format(repo, groupId.replace(".", "/"), artifactId, version, artifactId, version); 157 | return new URL(url); 158 | } 159 | 160 | @Override public String toString() { 161 | return "PluginLib{" + 162 | "groupId='" + groupId + '\'' + 163 | ", artifactId='" + artifactId + '\'' + 164 | ", version='" + version + '\'' + 165 | ", repository='" + repository + '\'' + 166 | ", relocationRules=" + relocationRules + 167 | ", hasRelocations=" + hasRelocations + 168 | '}'; 169 | } 170 | 171 | private static Method addURL; 172 | 173 | static { 174 | try { 175 | addURL = URLClassLoader.class.getDeclaredMethod("addURL", URL.class); 176 | addURL.setAccessible(true); 177 | } catch (Throwable e) { 178 | e.printStackTrace(); 179 | } 180 | } 181 | 182 | public static class Builder { 183 | 184 | private String url = null; 185 | private String group, artifact, version, repository = "https://repo1.maven.org/maven2/"; 186 | private final Set relocations = new LinkedHashSet<>(); 187 | 188 | protected Builder() { 189 | } 190 | 191 | /** 192 | * Sets the builder to create a static URL dependency. 193 | * 194 | * @param url URL of the dependency. 195 | * @return This builder 196 | */ 197 | public Builder fromURL(@NotNull String url) { 198 | this.url = n(url, "provided URL is null!"); 199 | return this; 200 | } 201 | 202 | /** 203 | * Sets the group ID of the dependency 204 | * 205 | * @param group New group ID to set 206 | * @return This builder 207 | */ 208 | public Builder groupId(@NotNull String group) { 209 | this.group = n(group, "groupId is null!"); 210 | return this; 211 | } 212 | 213 | /** 214 | * Sets the artifact ID of the dependency 215 | * 216 | * @param artifact New artifact ID to set 217 | * @return This builder 218 | */ 219 | public Builder artifactId(@NotNull String artifact) { 220 | this.artifact = n(artifact, "artifactId is null!"); 221 | return this; 222 | } 223 | 224 | /** 225 | * Sets the version of the dependency 226 | * 227 | * @param version New version to set 228 | * @return This builder 229 | */ 230 | public Builder version(@NotNull String version) { 231 | this.version = n(version, "version is null!"); 232 | return this; 233 | } 234 | 235 | /** 236 | * Sets the version of the dependency, by providing the major, minor, build numbers 237 | * 238 | * @param numbers An array of numbers to join using "." 239 | * @return This builder 240 | */ 241 | public Builder version(int... numbers) { 242 | StringJoiner version = new StringJoiner("."); 243 | for (int i : numbers) version.add(Integer.toString(i)); 244 | return version(version.toString()); 245 | } 246 | 247 | /** 248 | * Sets the repository to download the dependency from 249 | * 250 | * @param repository New repository to set 251 | * @return This builder 252 | */ 253 | public Builder repository(@NotNull String repository) { 254 | this.repository = requireNonNull(repository); 255 | return this; 256 | } 257 | 258 | /** 259 | * A convenience method to set the repository to JitPack 260 | * 261 | * @return This builder 262 | */ 263 | public Builder jitpack() { 264 | return repository("https://jitpack.io/"); 265 | } 266 | 267 | /** 268 | * A convenience method to set the repository to Bintray - JCenter 269 | * 270 | * @return This builder 271 | */ 272 | public Builder jcenter() { 273 | return repository("https://jcenter.bintray.com/"); 274 | } 275 | 276 | /** 277 | * A convenience method to set the repository to Maven Central 278 | * 279 | * @return This builder 280 | */ 281 | public Builder mavenCentral() { 282 | return repository("https://repo1.maven.org/maven2/"); 283 | } 284 | 285 | /** 286 | * A convenience method to set the repository to Aikar's Repository 287 | * 288 | * @return This builder 289 | */ 290 | public Builder aikar() { 291 | return repository("https://repo.aikar.co/content/groups/aikar/"); 292 | } 293 | 294 | /** 295 | * Adds a new relocation rule 296 | * 297 | * @param relocation New relocation rule to add 298 | * @return This builder 299 | */ 300 | public Builder relocate(@NotNull Relocation relocation) { 301 | relocations.add(n(relocation, "relocation is null!")); 302 | return this; 303 | } 304 | 305 | /** 306 | * Constructs a {@link PluginLib} from the provided values 307 | * 308 | * @return A new, immutable {@link PluginLib} instance. 309 | * @throws NullPointerException if any of the required properties is not provided. 310 | */ 311 | public PluginLib build() { 312 | if (url != null) 313 | return new StaticURLPluginLib(group, n(artifact, "artifactId"), n(version, "version"), repository, relocations, url); 314 | return new PluginLib(n(group, "groupId"), n(artifact, "artifactId"), n(version, "version"), n(repository, "repository"), relocations); 315 | } 316 | 317 | private static T n(T t, String m) { 318 | return requireNonNull(t, m); 319 | } 320 | 321 | } 322 | 323 | /** 324 | * A convenience method to check whether a class exists at runtime or not. 325 | * 326 | * @param className Class name to check for 327 | * @return true if the class exists, false if otherwise. 328 | */ 329 | public static boolean classExists(@NotNull String className) { 330 | try { 331 | Class.forName(className); 332 | return true; 333 | } catch (ClassNotFoundException e) { 334 | return false; 335 | } 336 | } 337 | 338 | private static final List toInstall = new ArrayList<>(); 339 | 340 | static void loadLibs() { 341 | libFile.get(); 342 | for (PluginLib pluginLib : toInstall) { 343 | pluginLib.load(DependentJavaPlugin.class); 344 | } 345 | } 346 | 347 | @SuppressWarnings({"FieldCanBeLocal", "FieldMayBeFinal"}) 348 | private static class LibrariesOptions { 349 | 350 | private String relocationPrefix = null; 351 | private String librariesFolder = "libs"; 352 | private boolean deleteAfterRelocation = false; 353 | private Map globalRelocations = Collections.emptyMap(); 354 | private Map libraries = Collections.emptyMap(); 355 | 356 | public static LibrariesOptions fromMap(@NotNull Map map) { 357 | LibrariesOptions options = new LibrariesOptions(); 358 | options.relocationPrefix = (String) map.get("relocation-prefix"); 359 | options.librariesFolder = (String) map.getOrDefault("libraries-folder", "libs"); 360 | options.globalRelocations = (Map) map.getOrDefault("global-relocations", Collections.emptyMap()); 361 | options.deleteAfterRelocation = (Boolean) map.getOrDefault("delete-after-relocation", false); 362 | options.libraries = new HashMap<>(); 363 | Map> declaredLibs = (Map>) map.get("libraries"); 364 | if (declaredLibs != null) 365 | for (Entry> lib : declaredLibs.entrySet()) { 366 | options.libraries.put(lib.getKey(), RuntimeLib.fromMap(lib.getValue())); 367 | } 368 | return options; 369 | } 370 | 371 | } 372 | 373 | @SuppressWarnings({"FieldCanBeLocal", "FieldMayBeFinal"}) 374 | private static class RuntimeLib { 375 | 376 | @Language("XML") private String xml = null; 377 | private String url = null; 378 | private String groupId = null, artifactId = null, version = null; 379 | private Map relocation = null; 380 | private String repository = null; 381 | 382 | Builder builder() { 383 | Builder b; 384 | if (url != null) 385 | b = fromURL(url); 386 | else if (xml != null) 387 | b = parseXML(xml); 388 | else 389 | b = new Builder(); 390 | if (groupId != null) b.groupId(groupId); 391 | if (artifactId != null) b.artifactId(artifactId); 392 | if (version != null) b.version(version); 393 | if (repository != null) b.repository(repository); 394 | return b; 395 | } 396 | 397 | static RuntimeLib fromMap(Map map) { 398 | RuntimeLib lib = new RuntimeLib(); 399 | lib.xml = (String) map.get("xml"); 400 | lib.url = (String) map.get("url"); 401 | lib.groupId = (String) map.get("groupId"); 402 | lib.artifactId = (String) map.get("artifactId"); 403 | lib.version = (String) map.get("version"); 404 | lib.repository = (String) map.get("repository"); 405 | lib.relocation = (Map) map.get("relocation"); 406 | return lib; 407 | } 408 | 409 | } 410 | 411 | private static class StaticURLPluginLib extends PluginLib { 412 | 413 | private final String url; 414 | 415 | public StaticURLPluginLib(String groupId, String artifactId, String version, String repository, Set relocationRules, String url) { 416 | super(groupId, artifactId, version, repository, relocationRules); 417 | this.url = url; 418 | } 419 | 420 | @Override public URL asURL() throws MalformedURLException { 421 | return new URL(url); 422 | } 423 | } 424 | 425 | private static final Supplier librariesOptions = memoize(() -> { 426 | Map map = (Map) new Yaml().load(new InputStreamReader(requireNonNull(DependentJavaPlugin.class.getClassLoader().getResourceAsStream("plugin.yml"), "Jar does not contain plugin.yml"))); 427 | 428 | String name = map.get("name").toString(); 429 | String folder = "libs"; 430 | if (map.containsKey("runtime-libraries")) 431 | return LibrariesOptions.fromMap(((Map) map.get("runtime-libraries"))); 432 | return null; 433 | }); 434 | 435 | private static final Supplier libFile = memoize(() -> { 436 | Map map = (Map) new Yaml().load(new InputStreamReader(requireNonNull(DependentJavaPlugin.class.getClassLoader().getResourceAsStream("plugin.yml"), "Jar does not contain plugin.yml"))); 437 | 438 | String name = map.get("name").toString(); 439 | 440 | LibrariesOptions options = librariesOptions.get(); 441 | 442 | String folder = options.librariesFolder; 443 | String prefix = options.relocationPrefix == null ? null : options.relocationPrefix; 444 | requireNonNull(prefix, "relocation-prefix must be defined in runtime-libraries!"); 445 | Set globalRelocations = new HashSet<>(); 446 | for (Entry global : options.globalRelocations.entrySet()) { 447 | globalRelocations.add(new Relocation(global.getKey(), prefix + "." + global.getValue())); 448 | } 449 | for (Entry lib : options.libraries.entrySet()) { 450 | RuntimeLib runtimeLib = lib.getValue(); 451 | Builder b = runtimeLib.builder(); 452 | if (runtimeLib.relocation != null && !runtimeLib.relocation.isEmpty()) 453 | for (Entry s : runtimeLib.relocation.entrySet()) { 454 | b.relocate(new Relocation(s.getKey(), prefix + "." + s.getValue())); 455 | } 456 | for (Relocation relocation : globalRelocations) { 457 | b.relocate(relocation); 458 | } 459 | toInstall.add(b.build()); 460 | } 461 | File file = new File(Bukkit.getUpdateFolderFile().getParentFile() + File.separator + name, folder); 462 | file.mkdirs(); 463 | return file; 464 | }); 465 | 466 | private static Supplier memoize(@NotNull Supplier delegate) { 467 | return new MemoizingSupplier<>(delegate); 468 | } 469 | 470 | // legally stolen from guava's Suppliers.memoize 471 | static class MemoizingSupplier implements Supplier, Serializable { 472 | 473 | final Supplier delegate; 474 | transient volatile boolean initialized; 475 | // "value" does not need to be volatile; visibility piggy-backs 476 | // on volatile read of "initialized". 477 | transient T value; 478 | 479 | MemoizingSupplier(Supplier delegate) { 480 | this.delegate = delegate; 481 | } 482 | 483 | @Override public T get() { 484 | // A 2-field variant of Double Checked Locking. 485 | if (!initialized) { 486 | synchronized (this) { 487 | if (!initialized) { 488 | T t = delegate.get(); 489 | value = t; 490 | initialized = true; 491 | return t; 492 | } 493 | } 494 | } 495 | return value; 496 | } 497 | } 498 | } 499 | -------------------------------------------------------------------------------- /src/main/java/pluginlib/Relocation.java: -------------------------------------------------------------------------------- 1 | package pluginlib; 2 | 3 | import java.util.Objects; 4 | 5 | /** 6 | * Represents a relocation rule. 7 | *

8 | * This class is immutable, hence is safe to share across threads. 9 | */ 10 | public final class Relocation { 11 | 12 | private final String path, newPath; 13 | 14 | /** 15 | * Creates a new relocation rule 16 | * 17 | * @param path Path to relocate 18 | * @param newPath New path to replace it 19 | */ 20 | public Relocation(String path, String newPath) { 21 | this.path = path.replace('/', '.').replace('#', '.'); 22 | this.newPath = newPath.replace('/', '.').replace('#', '.'); 23 | } 24 | 25 | public String getPath() { 26 | return path; 27 | } 28 | 29 | public String getNewPath() { 30 | return newPath; 31 | } 32 | 33 | @Override public String toString() { 34 | return String.format("Relocation{path='%s', newPath='%s'}", path, newPath); 35 | } 36 | 37 | @Override public boolean equals(Object o) { 38 | if (this == o) return true; 39 | if (!(o instanceof Relocation)) return false; 40 | Relocation that = (Relocation) o; 41 | return Objects.equals(path, that.path) && 42 | Objects.equals(newPath, that.newPath); 43 | } 44 | 45 | @Override public int hashCode() { 46 | return Objects.hash(path, newPath); 47 | } 48 | } 49 | --------------------------------------------------------------------------------