├── jarinjar ├── common │ ├── build.gradle │ └── src │ │ └── main │ │ └── java │ │ └── dev │ │ └── vankka │ │ └── dependencydownload │ │ └── jarinjar │ │ └── classloader │ │ └── JarInJarClassLoader.java ├── loader │ ├── build.gradle │ └── src │ │ └── main │ │ └── java │ │ └── dev │ │ └── vankka │ │ └── dependencydownload │ │ └── jarinjar │ │ └── loader │ │ ├── exception │ │ └── LoadingException.java │ │ └── ILoader.java ├── bootstrap │ ├── build.gradle │ └── src │ │ └── main │ │ └── java │ │ └── dev │ │ └── vankka │ │ └── dependencydownload │ │ └── jarinjar │ │ └── bootstrap │ │ ├── AbstractBootstrap.java │ │ └── classpath │ │ └── JarInJarClasspathAppender.java └── build.gradle ├── .github ├── FUNDING.yml └── workflows │ ├── gradle-wrapper-validation.yaml │ ├── build.yaml │ └── deploy.yaml ├── runtime ├── src │ ├── test │ │ ├── resources │ │ │ ├── invalid4.txt │ │ │ ├── invalid1.txt │ │ │ ├── invalid2.txt │ │ │ ├── invalid3.txt │ │ │ └── valid1.txt │ │ └── java │ │ │ └── dev │ │ │ └── vankka │ │ │ └── dependencydownload │ │ │ ├── ApplicationDependencyManagerTest.java │ │ │ ├── ResourceTest.java │ │ │ ├── Helpers.java │ │ │ ├── LoggerTest.java │ │ │ └── DependencyMangerTest.java │ └── main │ │ └── java │ │ └── dev │ │ └── vankka │ │ └── dependencydownload │ │ ├── classpath │ │ └── ClasspathAppender.java │ │ ├── path │ │ ├── CleanupPathProvider.java │ │ ├── DependencyPathProvider.java │ │ └── DirectoryDependencyPathProvider.java │ │ ├── classloader │ │ └── IsolatedClassLoader.java │ │ ├── logger │ │ └── Logger.java │ │ ├── repository │ │ ├── MavenRepository.java │ │ └── Repository.java │ │ ├── relocation │ │ └── Relocation.java │ │ ├── dependency │ │ ├── Dependency.java │ │ └── MavenDependency.java │ │ ├── resource │ │ └── DependencyDownloadResource.java │ │ ├── ApplicationDependencyManager.java │ │ └── DependencyManager.java └── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── logger ├── slf4j │ ├── build.gradle │ └── src │ │ └── main │ │ └── java │ │ └── dev │ │ └── vankka │ │ └── dependencydownload │ │ └── logger │ │ └── DependencyDownloadSlf4jLogger.java └── build.gradle ├── common ├── build.gradle └── src │ └── main │ └── java │ └── dev │ └── vankka │ └── dependencydownload │ └── common │ └── util │ └── HashUtil.java ├── settings.gradle ├── .gitignore ├── LICENSE ├── gradle-plugin ├── src │ └── main │ │ └── java │ │ └── dev │ │ └── vankka │ │ └── dependencydownload │ │ ├── inputs │ │ ├── ResourceSplittingStrategy.java │ │ └── Relocation.java │ │ ├── Dependency.java │ │ ├── DependencyDownloadGradlePlugin.java │ │ └── task │ │ └── GenerateDependencyDownloadResourceTask.java └── build.gradle ├── gradlew.bat ├── README.md └── gradlew /jarinjar/common/build.gradle: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: Vankka 2 | -------------------------------------------------------------------------------- /runtime/src/test/resources/invalid4.txt: -------------------------------------------------------------------------------- 1 | ===ALGORITHM SHA-256 2 | ===RELOCATIONS 3 | dev.vankka -------------------------------------------------------------------------------- /jarinjar/loader/build.gradle: -------------------------------------------------------------------------------- 1 | dependencies { 2 | api project(':jarinjar:jarinjar-common') 3 | } 4 | -------------------------------------------------------------------------------- /runtime/src/test/resources/invalid1.txt: -------------------------------------------------------------------------------- 1 | ===ALGORITHM SHA-256 2 | dev.vankka:dependencydownload-runtime:1.3.1 -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vankka/DependencyDownload/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /logger/slf4j/build.gradle: -------------------------------------------------------------------------------- 1 | dependencies { 2 | api project(':runtime') 3 | compileOnlyApi 'org.slf4j:slf4j-api:2.0.13' 4 | } 5 | -------------------------------------------------------------------------------- /jarinjar/bootstrap/build.gradle: -------------------------------------------------------------------------------- 1 | dependencies { 2 | api project(':runtime') 3 | compileOnlyApi project(':jarinjar:jarinjar-common') 4 | } 5 | -------------------------------------------------------------------------------- /runtime/src/test/resources/invalid2.txt: -------------------------------------------------------------------------------- 1 | dev.vankka:dependencydownload-runtime:1.3.1 8d0e52f1c260ff090fa8d3130ac299d1d1490fcc9ee0454dd846ad6be9e6fd7b -------------------------------------------------------------------------------- /common/build.gradle: -------------------------------------------------------------------------------- 1 | tasks.jar { 2 | manifest { 3 | attributes("Automatic-Module-Name": "dev.vankka.dependencydownload.common") 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /runtime/src/test/resources/invalid3.txt: -------------------------------------------------------------------------------- 1 | ===ALGORITHM SHA-256 2 | dev.vankka:dependencydownload-runtime 8d0e52f1c260ff090fa8d3130ac299d1d1490fcc9ee0454dd846ad6be9e6fd7b 3 | -------------------------------------------------------------------------------- /jarinjar/build.gradle: -------------------------------------------------------------------------------- 1 | // Empty project, no need to publish 2 | // only the subprojects of this project are relevant 3 | tasks.withType(PublishToMavenRepository).configureEach { 4 | it.enabled = false 5 | } -------------------------------------------------------------------------------- /logger/build.gradle: -------------------------------------------------------------------------------- 1 | // Empty project, no need to publish 2 | // only the subprojects of this project are relevant 3 | tasks.withType(PublishToMavenRepository).configureEach { 4 | it.enabled = false 5 | } -------------------------------------------------------------------------------- /runtime/src/test/resources/valid1.txt: -------------------------------------------------------------------------------- 1 | ===ALGORITHM SHA-256 2 | dev.vankka:dependencydownload-runtime:1.3.1 8d0e52f1c260ff090fa8d3130ac299d1d1490fcc9ee0454dd846ad6be9e6fd7b 3 | ===RELOCATIONS 4 | dev.vankka 5 | test.dev.vankka 6 | [] 7 | [] -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'DependencyDownload' 2 | 3 | [ 4 | 'common', 5 | 'gradle-plugin', 6 | 'runtime', 7 | 'logger:slf4j', 8 | 'jarinjar:common', 'jarinjar:bootstrap', 'jarinjar:loader' 9 | ].each { 10 | include it 11 | findProject(':' + it).name = String.join('-', it.split(':')) 12 | } 13 | -------------------------------------------------------------------------------- /.github/workflows/gradle-wrapper-validation.yaml: -------------------------------------------------------------------------------- 1 | name: Validate Gradle Wrapper 2 | 3 | on: 4 | push: 5 | paths: ["gradle/wrapper/**"] 6 | pull_request: 7 | paths: ["gradle/wrapper/**"] 8 | 9 | jobs: 10 | validation: 11 | name: "Validation" 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | - uses: gradle/actions/wrapper-validation@v3 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled class file 2 | *.class 3 | 4 | # Log file 5 | *.log 6 | 7 | # BlueJ files 8 | *.ctxt 9 | 10 | # Mobile Tools for Java (J2ME) 11 | .mtj.tmp/ 12 | 13 | # Package Files # 14 | *.jar 15 | *.war 16 | *.nar 17 | *.ear 18 | *.zip 19 | *.tar.gz 20 | *.rar 21 | 22 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 23 | hs_err_pid* 24 | 25 | .gradle 26 | **/build/ 27 | !src/**/build/ 28 | 29 | # Ignore Gradle GUI config 30 | gradle-app.setting 31 | 32 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 33 | !gradle-wrapper.jar 34 | 35 | # Cache of project 36 | .gradletasknamecache 37 | 38 | # IntelliJ 39 | .idea/ 40 | *.iml 41 | out/ -------------------------------------------------------------------------------- /runtime/build.gradle: -------------------------------------------------------------------------------- 1 | import org.apache.tools.ant.filters.ReplaceTokens 2 | 3 | apply plugin: 'java-library' 4 | 5 | dependencies { 6 | implementation project(':common') 7 | implementation 'me.lucko:jar-relocator:1.5' 8 | } 9 | 10 | var buildDir = layout.buildDirectory.get() 11 | tasks.register('generateSources', Copy) { 12 | from 'src/main/java' 13 | //include 'dev/vankka/dependencydownload/repository/Repository.java' 14 | into "$buildDir/generated-src" 15 | filter(ReplaceTokens, tokens: ['VERSION': project.version]) 16 | } 17 | compileJava.source = "$buildDir/generated-src" 18 | compileJava.dependsOn generateSources 19 | 20 | jar { 21 | manifest { 22 | attributes("Automatic-Module-Name": "dev.vankka.dependencydownload.runtime") 23 | } 24 | } -------------------------------------------------------------------------------- /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Checkout 10 | uses: actions/checkout@v4 11 | - name: Setup Java 12 | uses: actions/setup-java@v4 13 | with: 14 | distribution: 'temurin' 15 | java-version: 17 16 | cache: 'gradle' 17 | - name: Setup Gradle 18 | uses: gradle/actions/setup-gradle@v3 19 | - name: Build with Gradle 20 | run: ./gradlew build --no-daemon 21 | - name: Upload artifacts 22 | uses: actions/upload-artifact@v4 23 | with: 24 | name: build-artifacts 25 | path: "**/build/libs/*.jar" 26 | - name: Generate and submit dependency graph 27 | uses: gradle/actions/dependency-submission@v4 28 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yaml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | 3 | on: push 4 | 5 | jobs: 6 | deploy: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Checkout 10 | uses: actions/checkout@v4 11 | - name: Setup Java 12 | uses: actions/setup-java@v4 13 | with: 14 | distribution: 'temurin' 15 | java-version: 17 16 | cache: 'gradle' 17 | - name: Setup Gradle 18 | uses: gradle/actions/setup-gradle@v3 19 | - name: Deploy with Gradle 20 | run: ./gradlew publish --no-daemon 21 | env: 22 | ORG_GRADLE_PROJECT_sonatypeUsername: ${{ secrets.SONATYPE_USERNAME }} 23 | ORG_GRADLE_PROJECT_sonatypePassword: ${{ secrets.SONATYPE_PASSWORD }} 24 | SIGNING_KEY: ${{ secrets.SONATYPE_KEY }} 25 | SIGNING_KEY_PASS: ${{ secrets.SONATYPE_KEY_PASS }} 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Vankka 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /gradle-plugin/src/main/java/dev/vankka/dependencydownload/inputs/ResourceSplittingStrategy.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2021-2025 Vankka 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package dev.vankka.dependencydownload.inputs; 26 | 27 | public enum ResourceSplittingStrategy { 28 | 29 | SINGLE_FILE, 30 | TOP_LEVEL_DEPENDENCIES, 31 | ALL_DEPENDENCIES 32 | 33 | } 34 | -------------------------------------------------------------------------------- /jarinjar/loader/src/main/java/dev/vankka/dependencydownload/jarinjar/loader/exception/LoadingException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2021-2025 Vankka 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package dev.vankka.dependencydownload.jarinjar.loader.exception; 26 | 27 | public class LoadingException extends RuntimeException { 28 | 29 | public LoadingException(String message, Throwable cause) { 30 | super(message, cause); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /gradle-plugin/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java-gradle-plugin' 3 | id 'com.github.johnrengelman.shadow' version '8.1.1' 4 | id 'com.gradle.plugin-publish' version '1.1.0' apply false 5 | } 6 | 7 | repositories { 8 | gradlePluginPortal() 9 | } 10 | 11 | dependencies { 12 | compileOnly gradleApi() 13 | compileOnly 'com.github.johnrengelman:shadow:8.1.1' 14 | implementation project(':common') 15 | } 16 | 17 | java { 18 | withJavadocJar() 19 | withSourcesJar() 20 | } 21 | 22 | jar.finalizedBy shadowJar 23 | 24 | shadowJar { 25 | //noinspection GroovyAssignabilityCheck 26 | archiveClassifier = '' 27 | } 28 | 29 | gradlePlugin { 30 | plugins { 31 | create('runtimeDependencyDownload') { 32 | id = 'dev.vankka.dependencydownload.plugin' 33 | displayName = 'DependencyDownload Plugin' 34 | description = 'A plugin to generate a metadata file for downloading dependencies during runtime' 35 | implementationClass = 'dev.vankka.dependencydownload.DependencyDownloadGradlePlugin' 36 | tags.addAll('runtime', 'dependency', 'download') 37 | } 38 | } 39 | } 40 | 41 | if (!version.endsWith('-SNAPSHOT')) { 42 | apply plugin: 'com.gradle.plugin-publish' 43 | 44 | // Doesn't allow snapshot versions, so snapshots are put on sonatype 45 | gradlePlugin { 46 | website = 'https://github.com/Vankka/DependencyDownload' 47 | vcsUrl = 'https://github.com/Vankka/DependencyDownload' 48 | } 49 | tasks.withType(PublishToMavenRepository).configureEach { 50 | it.enabled = false 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /runtime/src/main/java/dev/vankka/dependencydownload/classpath/ClasspathAppender.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2021-2025 Vankka 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package dev.vankka.dependencydownload.classpath; 26 | 27 | import org.jetbrains.annotations.NotNull; 28 | 29 | import java.net.MalformedURLException; 30 | import java.nio.file.Path; 31 | 32 | /** 33 | * A helper class that appends a given {@link Path} to the classpath (for example by adding the path's url to a URLClassLoader). 34 | */ 35 | public interface ClasspathAppender { 36 | 37 | /** 38 | * Appends the given path to the classpath. 39 | * 40 | * @param path the path 41 | * @throws MalformedURLException in case the path needs to be turned into a URL, this can be thrown 42 | */ 43 | void appendFileToClasspath(@NotNull Path path) throws MalformedURLException; 44 | } 45 | -------------------------------------------------------------------------------- /runtime/src/main/java/dev/vankka/dependencydownload/path/CleanupPathProvider.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2021-2025 Vankka 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package dev.vankka.dependencydownload.path; 26 | 27 | import dev.vankka.dependencydownload.DependencyManager; 28 | import org.jetbrains.annotations.NotNull; 29 | 30 | import java.io.IOException; 31 | import java.nio.file.Path; 32 | import java.util.Collection; 33 | 34 | /** 35 | * An interface extending {@link DependencyPathProvider} to provide a cleanup path for the {@link DependencyManager}. 36 | * {@link DependencyManager#cleanupCacheDirectory()} requires the use of this interface in {@link DependencyManager#DependencyManager(DependencyPathProvider) DependencyManager}. 37 | */ 38 | public interface CleanupPathProvider extends DependencyPathProvider { 39 | 40 | @NotNull 41 | Collection getPathsForAllStoredDependencies() throws IOException; 42 | 43 | } 44 | -------------------------------------------------------------------------------- /runtime/src/main/java/dev/vankka/dependencydownload/path/DependencyPathProvider.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2021-2025 Vankka 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package dev.vankka.dependencydownload.path; 26 | 27 | import dev.vankka.dependencydownload.dependency.Dependency; 28 | import org.jetbrains.annotations.NotNull; 29 | 30 | import java.nio.file.Path; 31 | 32 | /** 33 | * A {@link Path} provider for {@link Dependency Dependencies}. 34 | */ 35 | @FunctionalInterface 36 | public interface DependencyPathProvider { 37 | 38 | static DirectoryDependencyPathProvider directory(Path directory) { 39 | return new DirectoryDependencyPathProvider(directory); 40 | } 41 | 42 | /** 43 | * Gets the path that should be used for the provided {@link Dependency}. 44 | * @param dependency the dependency 45 | * @param relocated if the path should be for the relocated or unrelocated file 46 | * @return The absolute or relative path for the provided dependency 47 | */ 48 | @NotNull 49 | Path getDependencyPath(@NotNull Dependency dependency, boolean relocated); 50 | 51 | } 52 | -------------------------------------------------------------------------------- /jarinjar/bootstrap/src/main/java/dev/vankka/dependencydownload/jarinjar/bootstrap/AbstractBootstrap.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2021-2025 Vankka 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package dev.vankka.dependencydownload.jarinjar.bootstrap; 26 | 27 | import dev.vankka.dependencydownload.jarinjar.classloader.JarInJarClassLoader; 28 | 29 | /** 30 | * A bootstrap that is loaded in by a {@code ILoader} from the loader module. 31 | */ 32 | @SuppressWarnings("unused") // API 33 | public abstract class AbstractBootstrap { 34 | 35 | private final JarInJarClassLoader classLoader; 36 | 37 | /** 38 | * The constructor. 39 | * @param classLoader the class that loaded in this bootstrap 40 | */ 41 | public AbstractBootstrap(JarInJarClassLoader classLoader) { 42 | this.classLoader = classLoader; 43 | } 44 | 45 | /** 46 | * Returns the {@link JarInJarClassLoader} that loaded in this bootstrap. 47 | * @return the {@link JarInJarClassLoader} 48 | */ 49 | @SuppressWarnings("unused") // API 50 | public JarInJarClassLoader getClassLoader() { 51 | return classLoader; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /runtime/src/main/java/dev/vankka/dependencydownload/classloader/IsolatedClassLoader.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2021-2025 Vankka 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package dev.vankka.dependencydownload.classloader; 26 | 27 | import dev.vankka.dependencydownload.classpath.ClasspathAppender; 28 | import org.jetbrains.annotations.NotNull; 29 | 30 | import java.net.MalformedURLException; 31 | import java.net.URL; 32 | import java.net.URLClassLoader; 33 | import java.nio.file.Path; 34 | 35 | /** 36 | * Utility {@link ClassLoader} to load classes onto a separate classpath as the main application. 37 | * Extends {@link ClasspathAppender} for use with {@link dev.vankka.dependencydownload.DependencyManager}. 38 | */ 39 | @SuppressWarnings("unused") // API 40 | public class IsolatedClassLoader extends URLClassLoader implements ClasspathAppender { 41 | 42 | static { 43 | ClassLoader.registerAsParallelCapable(); 44 | } 45 | 46 | public IsolatedClassLoader() { 47 | super(new URL[0], ClassLoader.getSystemClassLoader().getParent()); 48 | } 49 | 50 | @Override 51 | public void appendFileToClasspath(@NotNull Path path) throws MalformedURLException { 52 | addURL(path.toUri().toURL()); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /jarinjar/bootstrap/src/main/java/dev/vankka/dependencydownload/jarinjar/bootstrap/classpath/JarInJarClasspathAppender.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2021-2025 Vankka 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package dev.vankka.dependencydownload.jarinjar.bootstrap.classpath; 26 | 27 | import dev.vankka.dependencydownload.classpath.ClasspathAppender; 28 | import dev.vankka.dependencydownload.jarinjar.classloader.JarInJarClassLoader; 29 | import org.jetbrains.annotations.NotNull; 30 | 31 | import java.net.MalformedURLException; 32 | import java.nio.file.Path; 33 | 34 | /** 35 | * A {@link ClasspathAppender} for {@link JarInJarClassLoader}. 36 | */ 37 | @SuppressWarnings("unused") 38 | public class JarInJarClasspathAppender implements ClasspathAppender { 39 | 40 | private final JarInJarClassLoader classLoader; 41 | 42 | /** 43 | * Creates a new instance of this classpath appender. 44 | * @param classLoader the {@link JarInJarClassLoader} 45 | */ 46 | public JarInJarClasspathAppender(JarInJarClassLoader classLoader) { 47 | this.classLoader = classLoader; 48 | } 49 | 50 | @Override 51 | public void appendFileToClasspath(@NotNull Path path) throws MalformedURLException { 52 | classLoader.addURL(path); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /runtime/src/main/java/dev/vankka/dependencydownload/logger/Logger.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2021-2025 Vankka 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package dev.vankka.dependencydownload.logger; 26 | 27 | import dev.vankka.dependencydownload.dependency.Dependency; 28 | 29 | /** 30 | * Logger for DependencyDownload, override methods you want to log. 31 | */ 32 | @SuppressWarnings("unused") 33 | public interface Logger { 34 | 35 | /** 36 | * No operation logger, logs nothing. 37 | */ 38 | Logger NOOP = new Logger() {}; 39 | 40 | default void downloadStart() {} 41 | default void downloadEnd() {} 42 | 43 | default void downloadDependency(Dependency dependency) {} 44 | default void downloadSuccess(Dependency dependency) {} 45 | default void downloadFailed(Dependency dependency, Throwable throwable) {} 46 | 47 | default void relocateStart() {} 48 | default void relocateEnd() {} 49 | 50 | default void relocateDependency(Dependency dependency) {} 51 | default void relocateSuccess(Dependency dependency) {} 52 | default void relocateFailed(Dependency dependency, Throwable throwable) {} 53 | 54 | default void loadStart() {} 55 | default void loadEnd() {} 56 | 57 | default void loadDependency(Dependency dependency) {} 58 | default void loadSuccess(Dependency dependency) {} 59 | default void loadFailed(Dependency dependency, Throwable throwable) {} 60 | 61 | } 62 | -------------------------------------------------------------------------------- /runtime/src/main/java/dev/vankka/dependencydownload/repository/MavenRepository.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2021-2025 Vankka 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package dev.vankka.dependencydownload.repository; 26 | 27 | import dev.vankka.dependencydownload.dependency.Dependency; 28 | import dev.vankka.dependencydownload.dependency.MavenDependency; 29 | import org.jetbrains.annotations.NotNull; 30 | 31 | import java.net.MalformedURLException; 32 | import java.net.URL; 33 | 34 | @SuppressWarnings("unused") // API 35 | public class MavenRepository implements Repository { 36 | 37 | private final String host; 38 | 39 | /** 40 | * Create a standard repository. 41 | * @param host the host address, if it ends with {@code /} it will automatically be removed 42 | */ 43 | public MavenRepository(@NotNull String host) { 44 | this.host = host.endsWith("/") 45 | ? host.substring(0, host.length() - 1) 46 | : host; 47 | } 48 | 49 | @Override 50 | public String getHost() { 51 | return host; 52 | } 53 | 54 | @Override 55 | public URL createURL(Dependency dependency) throws MalformedURLException { 56 | if (!(dependency instanceof MavenDependency)) { 57 | throw new IllegalArgumentException("Not a MavenDependency"); 58 | } 59 | return new URL(getHost() + '/' + ((MavenDependency) dependency).getMavenPath()); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /runtime/src/test/java/dev/vankka/dependencydownload/ApplicationDependencyManagerTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2021-2025 Vankka 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package dev.vankka.dependencydownload; 26 | 27 | import org.junit.jupiter.api.Test; 28 | 29 | import java.util.Collections; 30 | 31 | import static dev.vankka.dependencydownload.Helpers.*; 32 | import static org.junit.jupiter.api.Assertions.*; 33 | 34 | public class ApplicationDependencyManagerTest { 35 | 36 | public ApplicationDependencyManagerTest() {} 37 | 38 | @Test 39 | public void addDependencyTest() { 40 | ApplicationDependencyManager manager = new ApplicationDependencyManager(PATH_PROVIDER); 41 | assertEquals(1, manager.include(Collections.singleton(FAKE_DEPENDENCY_1)).getDependencies().size()); 42 | } 43 | 44 | @Test 45 | public void duplicationTest() { 46 | ApplicationDependencyManager manager = new ApplicationDependencyManager(PATH_PROVIDER); 47 | assertEquals(1, manager.include(Collections.singleton(FAKE_DEPENDENCY_1)).getDependencies().size()); 48 | assertEquals(0, manager.include(Collections.singleton(FAKE_DEPENDENCY_1)).getDependencies().size()); 49 | } 50 | 51 | @Test 52 | public void multipleTest() { 53 | ApplicationDependencyManager manager = new ApplicationDependencyManager(PATH_PROVIDER); 54 | assertEquals(1, manager.include(Collections.singleton(FAKE_DEPENDENCY_1)).getDependencies().size()); 55 | assertEquals(1, manager.include(Collections.singleton(FAKE_DEPENDENCY_2)).getDependencies().size()); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /runtime/src/main/java/dev/vankka/dependencydownload/path/DirectoryDependencyPathProvider.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2021-2025 Vankka 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package dev.vankka.dependencydownload.path; 26 | 27 | import dev.vankka.dependencydownload.dependency.Dependency; 28 | import org.jetbrains.annotations.NotNull; 29 | 30 | import java.io.IOException; 31 | import java.nio.file.Files; 32 | import java.nio.file.Path; 33 | import java.util.Collection; 34 | import java.util.stream.Collectors; 35 | import java.util.stream.Stream; 36 | 37 | /** 38 | * Basic dependency path provider for saving files in a single directory. 39 | */ 40 | public class DirectoryDependencyPathProvider implements CleanupPathProvider { 41 | 42 | private static final String RELOCATED_FILE_PREFIX = "relocated_"; 43 | private final Path dependencyDirectory; 44 | 45 | /** 46 | * Creates a {@link DirectoryDependencyPathProvider}. 47 | * @param dependencyDirectory the directory used for downloaded and relocated dependencies. 48 | */ 49 | public DirectoryDependencyPathProvider(Path dependencyDirectory) { 50 | this.dependencyDirectory = dependencyDirectory; 51 | } 52 | 53 | @Override 54 | public @NotNull Path getDependencyPath(@NotNull Dependency dependency, boolean relocated) { 55 | return dependencyDirectory.resolve((relocated ? RELOCATED_FILE_PREFIX : "") + dependency.getStoredFileName()); 56 | } 57 | 58 | @Override 59 | public @NotNull Collection getPathsForAllStoredDependencies() throws IOException { 60 | try (Stream paths = Files.list(dependencyDirectory)) { 61 | return paths.collect(Collectors.toList()); 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /logger/slf4j/src/main/java/dev/vankka/dependencydownload/logger/DependencyDownloadSlf4jLogger.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2021-2025 Vankka 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package dev.vankka.dependencydownload.logger; 26 | 27 | import dev.vankka.dependencydownload.dependency.Dependency; 28 | 29 | @SuppressWarnings("unused") 30 | public class DependencyDownloadSlf4jLogger implements Logger { 31 | 32 | private final org.slf4j.Logger logger; 33 | 34 | public DependencyDownloadSlf4jLogger(org.slf4j.Logger logger) { 35 | this.logger = logger; 36 | } 37 | 38 | @Override 39 | public void downloadDependency(Dependency dependency) { 40 | logger.info("Downloading {}", dependency.getGAV()); 41 | } 42 | 43 | @Override 44 | public void downloadSuccess(Dependency dependency) { 45 | logger.info("Downloaded {}", dependency.getGAV()); 46 | } 47 | 48 | @Override 49 | public void downloadFailed(Dependency dependency, Throwable throwable) { 50 | logger.error("Failed to download {}", dependency.getGAV()); 51 | } 52 | 53 | @Override 54 | public void relocateStart() { 55 | logger.info("Relocating dependencies..."); 56 | } 57 | 58 | @Override 59 | public void relocateDependency(Dependency dependency) { 60 | logger.debug("Relocating {}", dependency.getGAV()); 61 | } 62 | 63 | @Override 64 | public void loadStart() { 65 | logger.info("Loading dependencies..."); 66 | } 67 | 68 | @Override 69 | public void loadDependency(Dependency dependency) { 70 | logger.debug("Loading {}", dependency.getGAV()); 71 | } 72 | 73 | @Override 74 | public void loadEnd() { 75 | logger.info("Loaded dependencies"); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /gradle-plugin/src/main/java/dev/vankka/dependencydownload/Dependency.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2021-2025 Vankka 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package dev.vankka.dependencydownload; 26 | 27 | import java.util.Objects; 28 | 29 | public class Dependency { 30 | 31 | private final String group; 32 | private final String module; 33 | private final String version; 34 | private final String classifier; 35 | private final String hash; 36 | 37 | public Dependency(String group, String module, String version, String classifier, String hash) { 38 | this.group = group; 39 | this.module = module; 40 | this.version = version; 41 | this.classifier = classifier; 42 | this.hash = hash; 43 | } 44 | 45 | public String getGroup() { 46 | return group; 47 | } 48 | 49 | public String getModule() { 50 | return module; 51 | } 52 | 53 | public String getVersion() { 54 | return version; 55 | } 56 | 57 | public String getClassifier() { 58 | return classifier; 59 | } 60 | 61 | public String getHash() { 62 | return hash; 63 | } 64 | 65 | @Override 66 | public String toString() { 67 | return group + ":" + module + ":" + version + (classifier != null ? ":" + classifier : "") + " " + hash; 68 | } 69 | 70 | @Override 71 | public boolean equals(Object o) { 72 | if (this == o) return true; 73 | if (o == null || getClass() != o.getClass()) return false; 74 | Dependency that = (Dependency) o; 75 | return group.equals(that.group) && module.equals(that.module) && version.equals( 76 | that.version) && Objects.equals(classifier, that.classifier); 77 | } 78 | 79 | @Override 80 | public int hashCode() { 81 | return Objects.hash(group, module, version, classifier); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /gradle-plugin/src/main/java/dev/vankka/dependencydownload/inputs/Relocation.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2021-2025 Vankka 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package dev.vankka.dependencydownload.inputs; 26 | 27 | import org.gradle.api.tasks.Input; 28 | 29 | import java.util.List; 30 | import java.util.Set; 31 | import java.util.stream.Collectors; 32 | 33 | public class Relocation { 34 | 35 | private final String pattern; 36 | private final String shadedPattern; 37 | private final Set includes; 38 | private final Set excludes; 39 | 40 | public Relocation() { 41 | this(null, null, null, null); 42 | } 43 | 44 | public Relocation(String pattern, String shadedPattern, List includes, List excludes) { 45 | this.pattern = pattern(pattern); 46 | this.shadedPattern = pattern(shadedPattern); 47 | this.includes = includes != null ? includes.stream().map(this::pattern).collect(Collectors.toSet()) : null; 48 | this.excludes = excludes != null ? excludes.stream().map(this::pattern).collect(Collectors.toSet()) : null; 49 | } 50 | 51 | private String pattern(String pattern) { 52 | if (pattern == null) { 53 | return null; 54 | } 55 | 56 | pattern = pattern.replace('/', '.'); 57 | if (pattern.endsWith(".")) { 58 | pattern = pattern.substring(0, pattern.length() - 1); 59 | } 60 | return pattern; 61 | } 62 | 63 | @Input 64 | public String getPattern() { 65 | return pattern; 66 | } 67 | 68 | @Input 69 | public String getShadedPattern() { 70 | return shadedPattern; 71 | } 72 | 73 | @Input 74 | public Set getIncludes() { 75 | return includes; 76 | } 77 | 78 | @Input 79 | public Set getExcludes() { 80 | return excludes; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /common/src/main/java/dev/vankka/dependencydownload/common/util/HashUtil.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2021-2025 Vankka 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package dev.vankka.dependencydownload.common.util; 26 | 27 | import org.jetbrains.annotations.ApiStatus; 28 | 29 | import java.io.File; 30 | import java.io.IOException; 31 | import java.io.InputStream; 32 | import java.nio.file.Files; 33 | import java.nio.file.Path; 34 | import java.security.MessageDigest; 35 | 36 | /** 37 | * A helper class to get standard hashes from {@link MessageDigest}s and {@link File}s. 38 | */ 39 | @ApiStatus.Internal 40 | public final class HashUtil { 41 | 42 | private HashUtil() {} 43 | 44 | /** 45 | * Gets the hash of the provided file 46 | * @param file the file 47 | * @param digest the message digest to use to make the hash 48 | * @return the file's hash in standard format 49 | * @throws IOException if reading the file was unsuccessful 50 | */ 51 | public static String getFileHash(Path file, MessageDigest digest) throws IOException { 52 | digest.reset(); 53 | 54 | try (InputStream inputStream = Files.newInputStream(file)) { 55 | byte[] buffer = new byte[1024]; 56 | int total; 57 | while ((total = inputStream.read(buffer)) != -1) { 58 | digest.update(buffer, 0, total); 59 | } 60 | } 61 | 62 | return getHash(digest); 63 | } 64 | 65 | /** 66 | * Gets the hash from the provided {@link MessageDigest}. 67 | * @param digest the message digest 68 | * @return the hash in standard format 69 | */ 70 | public static String getHash(MessageDigest digest) { 71 | StringBuilder result = new StringBuilder(); 72 | for (byte b : digest.digest()) { 73 | result.append(String.format("%02x", b)); 74 | } 75 | return result.toString(); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /runtime/src/test/java/dev/vankka/dependencydownload/ResourceTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2021-2025 Vankka 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package dev.vankka.dependencydownload; 26 | 27 | import dev.vankka.dependencydownload.resource.DependencyDownloadResource; 28 | import org.junit.jupiter.api.Test; 29 | 30 | import java.io.IOException; 31 | import java.net.URL; 32 | import java.util.Collections; 33 | 34 | import static dev.vankka.dependencydownload.Helpers.REAL_DEPENDENCY; 35 | import static dev.vankka.dependencydownload.Helpers.REAL_RELOCATION; 36 | import static org.junit.jupiter.api.Assertions.*; 37 | 38 | public class ResourceTest { 39 | 40 | private DependencyDownloadResource parseResource(String name) throws IOException { 41 | URL url = getClass().getClassLoader().getResource(name); 42 | assertNotNull(url, "resourceURL"); 43 | return DependencyDownloadResource.parse(url); 44 | } 45 | 46 | @Test 47 | public void validResourceTest1() throws IOException { 48 | DependencyDownloadResource parsed = parseResource("valid1.txt"); 49 | 50 | assertEquals(1, parsed.getDependencies().size()); 51 | assertEquals(1, parsed.getRelocations().size()); 52 | assertEquals(Collections.singletonList(REAL_DEPENDENCY), parsed.getDependencies()); 53 | assertEquals(Collections.singletonList(REAL_RELOCATION), parsed.getRelocations()); 54 | } 55 | 56 | @Test 57 | public void invalidResourcesTest() { 58 | assertThrows(IllegalArgumentException.class, () -> parseResource("invalid1.txt")); // No algorithm 59 | assertThrows(IllegalArgumentException.class, () -> parseResource("invalid2.txt")); // No hash 60 | assertThrows(IllegalArgumentException.class, () -> parseResource("invalid3.txt")); // No version 61 | assertThrows(IllegalArgumentException.class, () -> parseResource("invalid4.txt")); // No relocation replacement 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /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 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 1>&2 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 48 | echo. 1>&2 49 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 50 | echo location of your Java installation. 1>&2 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 1>&2 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 62 | echo. 1>&2 63 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 64 | echo location of your Java installation. 1>&2 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /runtime/src/main/java/dev/vankka/dependencydownload/relocation/Relocation.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2021-2025 Vankka 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package dev.vankka.dependencydownload.relocation; 26 | 27 | import org.jetbrains.annotations.NotNull; 28 | import org.jetbrains.annotations.Nullable; 29 | 30 | import java.util.Collections; 31 | import java.util.Objects; 32 | import java.util.Set; 33 | 34 | /** 35 | * A relocation. 36 | */ 37 | public class Relocation { 38 | 39 | private final String pattern; 40 | private final String shadedPattern; 41 | private final Set includes; 42 | private final Set excludes; 43 | 44 | public Relocation(@NotNull String pattern, @NotNull String shadedPattern, @Nullable Set includes, @Nullable Set excludes) { 45 | this.pattern = pattern; 46 | this.shadedPattern = shadedPattern; 47 | this.includes = includes != null ? includes : Collections.emptySet(); 48 | this.excludes = excludes != null ? excludes : Collections.emptySet(); 49 | } 50 | 51 | @NotNull 52 | public String getPattern() { 53 | return pattern; 54 | } 55 | 56 | @NotNull 57 | public String getShadedPattern() { 58 | return shadedPattern; 59 | } 60 | 61 | @NotNull 62 | public Set getIncludes() { 63 | return includes; 64 | } 65 | 66 | @NotNull 67 | public Set getExcludes() { 68 | return excludes; 69 | } 70 | 71 | @Override 72 | public boolean equals(Object o) { 73 | if (this == o) return true; 74 | if (o == null || getClass() != o.getClass()) return false; 75 | Relocation that = (Relocation) o; 76 | return Objects.equals(pattern, that.pattern) 77 | && Objects.equals(shadedPattern, that.shadedPattern) 78 | && Objects.equals(includes, that.includes) 79 | && Objects.equals(excludes, that.excludes); 80 | } 81 | 82 | @Override 83 | public int hashCode() { 84 | return Objects.hash(pattern, shadedPattern, includes, excludes); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /runtime/src/test/java/dev/vankka/dependencydownload/Helpers.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2021-2025 Vankka 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package dev.vankka.dependencydownload; 26 | 27 | import dev.vankka.dependencydownload.dependency.Dependency; 28 | import dev.vankka.dependencydownload.dependency.MavenDependency; 29 | import dev.vankka.dependencydownload.path.DependencyPathProvider; 30 | import dev.vankka.dependencydownload.relocation.Relocation; 31 | import dev.vankka.dependencydownload.repository.MavenRepository; 32 | import dev.vankka.dependencydownload.repository.Repository; 33 | 34 | import java.io.IOException; 35 | import java.net.URLConnection; 36 | import java.nio.file.Paths; 37 | 38 | public final class Helpers { 39 | 40 | private Helpers() {} 41 | 42 | public static final DependencyPathProvider PATH_PROVIDER = DependencyPathProvider.directory(Paths.get("build", "integration-test")); 43 | public static final DependencyPathProvider PATH_PROVIDER_FOR_CLEANUP = DependencyPathProvider.directory(Paths.get("build", "integration-test-cleanup")); 44 | 45 | public static final Dependency FAKE_DEPENDENCY_1 = new MavenDependency("a", "a-a", "", "", "", "SHA-256"); 46 | public static final Dependency FAKE_DEPENDENCY_2 = new MavenDependency("b", "b-a", "", "", "", "SHA-256"); 47 | 48 | public static final Repository REAL_REPOSITORY = new MavenRepository("https://repo1.maven.org/maven2"); 49 | public static final Dependency REAL_DEPENDENCY = new MavenDependency( 50 | "dev.vankka", 51 | "dependencydownload-runtime", 52 | "1.3.1", 53 | null, 54 | "8d0e52f1c260ff090fa8d3130ac299d1d1490fcc9ee0454dd846ad6be9e6fd7b", 55 | "SHA-256" 56 | ); 57 | public static final Relocation REAL_RELOCATION = new Relocation("dev.vankka", "test.dev.vankka", null, null); 58 | 59 | public static final Repository FAKE_REPOSITORY = new MavenRepository("") { 60 | @Override 61 | public URLConnection openConnection(Dependency dependency) throws IOException { 62 | throw new IOException("Failed"); 63 | } 64 | }; 65 | } 66 | -------------------------------------------------------------------------------- /gradle-plugin/src/main/java/dev/vankka/dependencydownload/DependencyDownloadGradlePlugin.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2021-2025 Vankka 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package dev.vankka.dependencydownload; 26 | 27 | import dev.vankka.dependencydownload.task.GenerateDependencyDownloadResourceTask; 28 | import org.gradle.api.Plugin; 29 | import org.gradle.api.Project; 30 | import org.gradle.api.artifacts.Configuration; 31 | import org.gradle.api.artifacts.ConfigurationContainer; 32 | import org.gradle.api.tasks.TaskContainer; 33 | 34 | import java.util.HashMap; 35 | import java.util.Map; 36 | 37 | public class DependencyDownloadGradlePlugin implements Plugin { 38 | 39 | public static final String BASE_CONFIGURATION_NAME = "runtimeDownloadOnly"; 40 | public static final String COMPILE_CONFIGURATION_NAME = "runtimeDownload"; 41 | 42 | @Override 43 | public void apply(Project project) { 44 | // 45 | // Configurations 46 | // 47 | ConfigurationContainer configurations = project.getConfigurations(); 48 | Configuration baseConfiguration = configurations.create(BASE_CONFIGURATION_NAME); 49 | 50 | Configuration compileConfiguration = configurations.create(COMPILE_CONFIGURATION_NAME); 51 | configurations.getByName("compileOnly").extendsFrom(compileConfiguration); 52 | 53 | baseConfiguration.extendsFrom(compileConfiguration); 54 | 55 | // 56 | // Tasks 57 | // 58 | Map tasksToMake = new HashMap<>(); 59 | String taskName = "generateRuntimeDownloadResourceFor"; 60 | tasksToMake.put(taskName + "RuntimeDownloadOnly", baseConfiguration); 61 | tasksToMake.put(taskName + "RuntimeDownload", compileConfiguration); 62 | 63 | TaskContainer tasks = project.getTasks(); 64 | for (Map.Entry entry : tasksToMake.entrySet()) { 65 | String configurationName = entry.getKey(); 66 | Configuration configuration = entry.getValue(); 67 | tasks.register(configurationName, GenerateDependencyDownloadResourceTask.class, t -> t.configuration(configuration)); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /runtime/src/main/java/dev/vankka/dependencydownload/repository/Repository.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2021-2025 Vankka 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package dev.vankka.dependencydownload.repository; 26 | 27 | import dev.vankka.dependencydownload.dependency.Dependency; 28 | 29 | import javax.net.ssl.HttpsURLConnection; 30 | import java.io.IOException; 31 | import java.net.HttpURLConnection; 32 | import java.net.MalformedURLException; 33 | import java.net.URL; 34 | import java.net.URLConnection; 35 | 36 | public interface Repository { 37 | 38 | /** 39 | * The maven repository host's address. 40 | * @return the host's address, without the path to the actual dependency and slash before the path 41 | */ 42 | String getHost(); 43 | 44 | /** 45 | * Creates an url for the given {@link Dependency} with the {@link #getHost()}. 46 | * 47 | * @param dependency the dependency to generate the url for 48 | * @return the url 49 | * @throws MalformedURLException if the url syntax is invalid 50 | */ 51 | URL createURL(Dependency dependency) throws MalformedURLException; 52 | 53 | /** 54 | * Opens a connection from an url generated with {@link #createURL(Dependency)}. 55 | * 56 | * @param dependency the dependency used to generate the url 57 | * @return the opened {@link HttpsURLConnection} 58 | * @throws IOException if opening connection fails 59 | */ 60 | default URLConnection openConnection(Dependency dependency) throws IOException { 61 | URLConnection connection = createURL(dependency).openConnection(); 62 | 63 | if (connection instanceof HttpURLConnection) { 64 | if (!(connection instanceof HttpsURLConnection)) { 65 | throw new RuntimeException("HTTP is not supported."); 66 | } 67 | connection.setRequestProperty("User-Agent", "DependencyDownload/@VERSION@"); 68 | } 69 | return connection; 70 | } 71 | 72 | /** 73 | * Gets the default buffer size (in bytes) for downloading files from this repository, defaults to {@code 8192}. 74 | * @return the buffer size 75 | */ 76 | default int getBufferSize() { 77 | return 8192; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /jarinjar/common/src/main/java/dev/vankka/dependencydownload/jarinjar/classloader/JarInJarClassLoader.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2021-2025 Vankka 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package dev.vankka.dependencydownload.jarinjar.classloader; 26 | 27 | import java.io.IOException; 28 | import java.io.InputStream; 29 | import java.net.MalformedURLException; 30 | import java.net.URISyntaxException; 31 | import java.net.URL; 32 | import java.net.URLClassLoader; 33 | import java.nio.file.Files; 34 | import java.nio.file.Path; 35 | import java.nio.file.Paths; 36 | import java.nio.file.StandardCopyOption; 37 | 38 | /** 39 | * Loads a jar from a resource into a temporary file to avoid illegal reflection. 40 | * You can find the {@code JarInJarClasspathAppender} in the bootstrap module. 41 | */ 42 | public class JarInJarClassLoader extends URLClassLoader { 43 | 44 | static { 45 | ClassLoader.registerAsParallelCapable(); 46 | } 47 | 48 | public JarInJarClassLoader(String tempFilePrefix, URL resourceURL, ClassLoader parent) throws IOException { 49 | super(new URL[] {asTempFileURL(tempFilePrefix, resourceURL)}, parent); 50 | } 51 | 52 | private static URL asTempFileURL(String filePrefix, URL resourceURL) throws IOException { 53 | if (filePrefix == null) { 54 | throw new NullPointerException("filePrefix"); 55 | } else if (resourceURL == null) { 56 | throw new NullPointerException("resourceURL"); 57 | } 58 | 59 | Path tempFile = Files.createTempFile(filePrefix, ".jar.tmp"); 60 | try (InputStream inputStream = resourceURL.openStream()) { 61 | Files.copy(inputStream, tempFile, StandardCopyOption.REPLACE_EXISTING); 62 | } 63 | 64 | tempFile.toFile().deleteOnExit(); 65 | return tempFile.toUri().toURL(); 66 | } 67 | 68 | @Override 69 | public void close() throws IOException { 70 | super.close(); 71 | 72 | URL url = getURLs()[0]; 73 | if (url != null) { 74 | Path path; 75 | try { 76 | path = Paths.get(url.toURI()); 77 | } catch (URISyntaxException ignored) { 78 | return; 79 | } 80 | Files.deleteIfExists(path); 81 | } 82 | } 83 | 84 | public void addURL(Path path) throws MalformedURLException { 85 | addURL(path.toUri().toURL()); 86 | } 87 | 88 | } 89 | -------------------------------------------------------------------------------- /runtime/src/main/java/dev/vankka/dependencydownload/dependency/Dependency.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2021-2025 Vankka 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package dev.vankka.dependencydownload.dependency; 26 | 27 | import org.jetbrains.annotations.NotNull; 28 | import org.jetbrains.annotations.Nullable; 29 | 30 | /** 31 | * A dependency. 32 | */ 33 | public interface Dependency { 34 | 35 | /** 36 | * The group id of the dependency. 37 | * @return the dependency's group id 38 | */ 39 | @NotNull 40 | String getGroupId(); 41 | 42 | /** 43 | * The artifact id of the dependency. 44 | * @return the dependency's artifact id 45 | */ 46 | @NotNull 47 | String getArtifactId(); 48 | 49 | /** 50 | * The version of the dependency. 51 | * @return the dependency's version 52 | */ 53 | @NotNull 54 | String getVersion(); 55 | 56 | /** 57 | * The classifier for the dependency artifact, if any. 58 | * @return the dependency artifact's classifier or {@code null}. 59 | */ 60 | @Nullable 61 | String getClassifier(); 62 | 63 | /** 64 | * The timestamped snapshot version. 65 | * @return the timestamped snapshot version or {@code null} if this isn't a snapshot dependency. 66 | * @see #isSnapshot() 67 | */ 68 | @Nullable 69 | String getSnapshotVersion(); 70 | 71 | /** 72 | * The hash of the dependency, this is checked against the downloaded file. 73 | * @return the hash of the dependency archive 74 | * @see #getHashingAlgorithm() 75 | */ 76 | @NotNull 77 | String getHash(); 78 | 79 | /** 80 | * The hashing algorithm used for the {@link #getHash()}. 81 | * @return the hashing algorithm used for the dependency archive's hash 82 | * @see #getHash() 83 | */ 84 | @NotNull 85 | String getHashingAlgorithm(); 86 | 87 | /** 88 | * If this is a snapshot dependency. 89 | * @return true if this dependency is a snapshot 90 | */ 91 | default boolean isSnapshot() { 92 | return getSnapshotVersion() != null; 93 | } 94 | 95 | /** 96 | * Returns the file name for the end of the maven path. 97 | * @return the file name for the dependency 98 | */ 99 | @NotNull 100 | default String getFileName() { 101 | String classifier = getClassifier(); 102 | String snapshotVersion = getSnapshotVersion(); 103 | return getArtifactId() 104 | + '-' + (snapshotVersion != null ? snapshotVersion : getVersion()) 105 | + (classifier != null ? '-' + classifier : "") 106 | + ".jar"; 107 | } 108 | 109 | /** 110 | * Returns the file name when stored to disk. 111 | * @return the file name for storing the dependency 112 | */ 113 | @NotNull 114 | default String getStoredFileName() { 115 | String classifier = getClassifier(); 116 | return getGroupId() 117 | + '-' + getArtifactId() 118 | + '-' + getVersion() 119 | + (isSnapshot() ? "-" + getSnapshotVersion() : "") 120 | + (classifier != null ? '-' + classifier : "") 121 | + ".jar"; 122 | } 123 | 124 | /** 125 | * Gets the group id, artifact id, version and classifier (if specified) seperated by semicolons. 126 | * @return the artifact's GAV and classifier parameters seperated by semicolons (:) 127 | */ 128 | @NotNull 129 | default String getGAV() { 130 | String classifier = getClassifier(); 131 | return getGroupId() + ":" + getArtifactId() + ":" + getVersion() + (classifier != null ? ":" + classifier : ""); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /runtime/src/main/java/dev/vankka/dependencydownload/dependency/MavenDependency.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2021-2025 Vankka 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package dev.vankka.dependencydownload.dependency; 26 | 27 | import org.intellij.lang.annotations.Pattern; 28 | import org.jetbrains.annotations.NotNull; 29 | import org.jetbrains.annotations.Nullable; 30 | 31 | import java.util.Objects; 32 | 33 | public class MavenDependency implements Dependency { 34 | 35 | private static final String MAVEN_PATH_FORMAT = "%s/%s/%s/%s"; 36 | 37 | private final String groupId; 38 | private final String artifactId; 39 | private final String version; 40 | private final String classifier; 41 | private final String hash; 42 | private final String hashingAlgorithm; 43 | private final String snapshotVersion; 44 | 45 | public MavenDependency( 46 | @NotNull String groupId, 47 | @NotNull String artifactId, 48 | @NotNull String version, 49 | @Nullable String classifier, 50 | @NotNull String hash, 51 | @NotNull String hashingAlgorithm 52 | ) { 53 | this(groupId, artifactId, version, classifier, null, hash, hashingAlgorithm); 54 | } 55 | 56 | public MavenDependency( 57 | @NotNull String groupId, 58 | @NotNull String artifactId, 59 | @NotNull String version, 60 | @Nullable String classifier, 61 | @Nullable @Pattern("[0-9]{8}\\.[0-9]{6}-[0-9]+") String snapshotVersion, 62 | @NotNull String hash, 63 | @NotNull String hashingAlgorithm 64 | ) { 65 | this.groupId = groupId; 66 | this.artifactId = artifactId; 67 | this.version = version; 68 | this.classifier = classifier; 69 | this.snapshotVersion = snapshotVersion; 70 | this.hash = hash; 71 | this.hashingAlgorithm = hashingAlgorithm; 72 | } 73 | 74 | @Override 75 | public @NotNull String getGroupId() { 76 | return groupId; 77 | } 78 | 79 | @Override 80 | public @NotNull String getArtifactId() { 81 | return artifactId; 82 | } 83 | 84 | @Override 85 | public @NotNull String getVersion() { 86 | return version; 87 | } 88 | 89 | @Override 90 | public String getClassifier() { 91 | return classifier; 92 | } 93 | 94 | @Override 95 | public String getSnapshotVersion() { 96 | return snapshotVersion; 97 | } 98 | 99 | @Override 100 | public @NotNull String getHash() { 101 | return hash; 102 | } 103 | 104 | @Override 105 | public @NotNull String getHashingAlgorithm() { 106 | return hashingAlgorithm; 107 | } 108 | 109 | /** 110 | * The path to this dependency on a maven repository, without the protocol, domain or slash at the beginning. 111 | * @return the path to this dependency's jar file on a maven repository 112 | */ 113 | @NotNull 114 | public String getMavenPath() { 115 | return String.format( 116 | MAVEN_PATH_FORMAT, 117 | getGroupId().replace('.', '/'), 118 | getArtifactId(), 119 | getVersion(), 120 | getFileName() 121 | ); 122 | } 123 | 124 | @Override 125 | public boolean equals(Object o) { 126 | if (this == o) return true; 127 | if (o == null || getClass() != o.getClass()) return false; 128 | MavenDependency that = (MavenDependency) o; 129 | return Objects.equals(groupId, that.groupId) 130 | && Objects.equals(artifactId, that.artifactId) 131 | && Objects.equals(version, that.version) 132 | && Objects.equals(classifier, that.classifier) 133 | && Objects.equals(snapshotVersion, that.snapshotVersion); 134 | } 135 | 136 | @Override 137 | public int hashCode() { 138 | return Objects.hash(groupId, artifactId, version, classifier, snapshotVersion); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DependencyDownload 2 | [![Maven Central](https://img.shields.io/maven-central/v/dev.vankka/dependencydownload-runtime?label=release)](https://central.sonatype.com/search?q=g%253Adev.vankka+dependencydownload) 3 | ![Sonatype (Snapshots)](https://img.shields.io/maven-metadata/v?metadataUrl=https%3A%2F%2Fcentral.sonatype.com%2Frepository%2Fmaven-snapshots%2Fdev%2Fvankka%2Fdependencydownload-runtime%2Fmaven-metadata.xml&label=snapshot) 4 | [![Gradle Plugin Portal](https://img.shields.io/gradle-plugin-portal/v/dev.vankka.dependencydownload.plugin?label=gradle%20plugin)](https://plugins.gradle.org/plugin/dev.vankka.dependencydownload.plugin) 5 | 6 | A library to download, relocate & load dependencies during runtime for Maven dependencies for JVM applications. 7 | There is also a Gradle plugin to generate a metadata file, to avoid having to define the dependencies in code. 8 | 9 | Uses [jar-relocator](https://github.com/lucko/jar-relocator/) for relocations during runtime 10 | Looking for something to use with Minecraft? [Check out MinecraftDependencyDownload](https://github.com/Vankka/MinecraftDependencyDownload/) 11 | 12 | ## Dependency 13 | 14 | ```groovy 15 | repositories { 16 | mavenCentral() 17 | } 18 | 19 | dependencies { 20 | implementation 'dev.vankka:dependencydownload-runtime:2.0.0' 21 | } 22 | ``` 23 | 24 |
25 | Snapshots 26 | 27 | ```groovy 28 | repositories { 29 | maven { 30 | url 'https://central.sonatype.com/repository/maven-snapshots/' 31 | } 32 | } 33 | 34 | dependencies { 35 | implementation 'dev.vankka:dependencydownload-runtime:2.0.1-SNAPSHOT' 36 | } 37 | ``` 38 |
39 | 40 | ## Usage 41 | ```java 42 | 43 | DependencyManager manager = new DependencyManager(DependencyPathProvider.directory(Paths.get("cache"))); 44 | manager.addDependencies(new StandardDependency("com.example", "examplepackage", "1.0.0", "", "SHA-256")); 45 | manager.addRelocations(new Relocation("com.example", "relocated.com.example")); 46 | 47 | Executor executor = Executors.newCachedThreadPool(); 48 | 49 | // All of these return CompletableFuture it is important to let the previous step finishing before starting the next 50 | manager.downloadAll(executor, Collections.singletonList(new StandardRepository("https://repo.example.com/maven2"))).join(); 51 | manager.relocateAll(executor).join(); 52 | manager.loadAll(executor, classpathAppender).join(); // ClasspathAppender is a interface that you need to implement to append a Path to the classpath 53 | ``` 54 | 55 | ## Gradle plugin 56 | ```groovy 57 | plugins { 58 | id 'dev.vankka.dependencydownload.plugin' version '2.0.0' 59 | } 60 | 61 | dependencies { 62 | runtimeDownload 'some:dependency:x.y.z' 63 | runtimeDownloadOnly 'some.other:dependency:x.y.z' 64 | } 65 | 66 | jar.dependsOn generateRuntimeDownloadResourceForRuntimeDownloadOnly, generateRuntimeDownloadResourceForRuntimeDownload 67 | ``` 68 | This would generate two files `runtimeDownloadOnly.txt` and `runtimeDownload.txt` 69 | 70 | ### ShadowJar support 71 | [shadow](https://github.com/johnrengelman/shadow) is supported to include the relocations in the generated metadata files 72 | 73 | ### Loading dependencies from a plugin generated file 74 | ```java 75 | DependencyManager manager = new DependencyManager(DependencyPathProvider.directory(Paths.get("cache"))); 76 | manager.loadResource(DependencyDownloadResource.parse(getClass().getResource("runtimeDownloadOnly.txt"))); 77 | ``` 78 | 79 | ### Customizing 80 | ```groovy 81 | import dev.vankka.dependencydownload.task.GenerateDependencyDownloadResourceTask 82 | 83 | task generateResource(type: GenerateDependencyDownloadResourceTask) { 84 | configuration = project.configurations.runtimeDownload 85 | includeRelocations = true 86 | hashingAlgorithm = 'SHA-256' 87 | file = 'dependencies.txt' 88 | } 89 | ``` 90 | 91 | ## Download `jar-relocator` during runtime 92 | Bring the jar minifying to the next extreme 93 | ```groovy 94 | import dev.vankka.dependencydownload.task.GenerateDependencyDownloadResourceTask 95 | plugins { 96 | id 'dev.vankka.dependencydownload.plugin' version '2.0.0' 97 | } 98 | 99 | configurations { 100 | jarRelocator 101 | } 102 | 103 | repositories { 104 | mavenCentral() 105 | } 106 | 107 | dependencies { 108 | implementation('dev.vankka:dependencydownload-runtime:2.0.0') { 109 | exclude module: 'jar-relocator' 110 | } 111 | jarRelocator 'me.lucko:jar-relocator:1.4' 112 | } 113 | 114 | task generateJarRelocatorResource(type: GenerateDependencyDownloadResourceTask) { 115 | configuration = project.configurations.jarRelocator 116 | } 117 | processResources.dependsOn generateJarRelocatorResource 118 | ``` 119 | 120 | ```java 121 | DependencyManager manager = new DependencyManager(DependencyPathProvider.directory(Paths.get("cache"))); 122 | manager.loadResource(DependencyDownloadResource.parse(getClass().getResource("jarRelocator.txt"))); 123 | 124 | Executor executor = Executors.newCachedThreadPool(2); 125 | 126 | manager.downloadAll(executor, Collections.singletonList(new StandardRepository("https://repo.example.com/maven2"))).join(); 127 | manager.loadAll(executor, classpathAppender).join(); 128 | // now jar-relocator is in the classpath and we can load (and relocate) dependencies from a regular configuration 129 | ``` 130 | -------------------------------------------------------------------------------- /jarinjar/loader/src/main/java/dev/vankka/dependencydownload/jarinjar/loader/ILoader.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2021-2025 Vankka 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package dev.vankka.dependencydownload.jarinjar.loader; 26 | 27 | import dev.vankka.dependencydownload.jarinjar.classloader.JarInJarClassLoader; 28 | import dev.vankka.dependencydownload.jarinjar.loader.exception.LoadingException; 29 | import org.jetbrains.annotations.ApiStatus; 30 | import org.jetbrains.annotations.MustBeInvokedByOverriders; 31 | import org.jetbrains.annotations.NotNull; 32 | import org.jetbrains.annotations.UnknownNullability; 33 | 34 | import java.io.IOException; 35 | import java.net.URL; 36 | 37 | /** 38 | * A bootstrap loader, {@link #initialize()} should be called from the constructor. 39 | */ 40 | @SuppressWarnings("unused") // API 41 | public interface ILoader { 42 | 43 | /** 44 | * Initializes the {@link JarInJarClassLoader} and loads in the {@link #getBootstrapClassName()}, and then calls {@link #initiateBootstrap(Class, JarInJarClassLoader)} with that class the class loader made previously. 45 | * Calls {@link #handleLoadingException(LoadingException)} if initialization fails, by default this throws the exception 46 | * 47 | * @return the {@link JarInJarClassLoader} used to load the bootstrap, this should be closed when not needed anymore 48 | * 49 | * @see #createClassLoader() 50 | * @see #initiateBootstrap(Class, JarInJarClassLoader) 51 | */ 52 | @MustBeInvokedByOverriders 53 | @ApiStatus.NonExtendable 54 | @UnknownNullability("Not null unless handleLoadingException is modified") 55 | default JarInJarClassLoader initialize() { 56 | try { 57 | JarInJarClassLoader classLoader = createClassLoader(); 58 | Class clazz = classLoader.loadClass(getBootstrapClassName()); 59 | initiateBootstrap(clazz, classLoader); 60 | 61 | return classLoader; 62 | } catch (Throwable t) { 63 | handleLoadingException(new LoadingException("Unable to load JarInJar", t)); 64 | return null; 65 | } 66 | } 67 | 68 | /** 69 | * {@link LoadingException} handler for this {@link ILoader}, by default the exception is simply thrown into the constructor through {@link #initialize()}. 70 | * @param exception the loading exception 71 | */ 72 | default void handleLoadingException(@NotNull LoadingException exception) { 73 | throw exception; 74 | } 75 | 76 | /** 77 | * Creates the class loader for this {@link ILoader}. 78 | * @return the new classloader 79 | * @throws IOException if creating a file in the temporary file directory fails 80 | */ 81 | @NotNull 82 | default JarInJarClassLoader createClassLoader() throws IOException { 83 | return new JarInJarClassLoader(getName(), getJarInJarResource(), getParentClassLoader()); 84 | } 85 | 86 | /** 87 | * The parent {@link ClassLoader} that loaded in this loader, this is used in {@link #createClassLoader()} by default. 88 | * @return the parent {@link ClassLoader} 89 | */ 90 | @NotNull 91 | default ClassLoader getParentClassLoader() { 92 | return getClass().getClassLoader(); 93 | } 94 | 95 | /** 96 | * Get the constructor from the class and create a new instance, the provided {@link JarInJarClassLoader} should be used in the constructor. 97 | * 98 | * @param bootstrapClass the bootstrap class resolved from {@link #getBootstrapClassName()} 99 | * @param classLoader the {@link JarInJarClassLoader} that was used to load the bootstrap class and should be provided in the constructor 100 | * @throws ReflectiveOperationException a convenience throw, will be passed as a {@link LoadingException} to {@link #handleLoadingException(LoadingException)} 101 | */ 102 | void initiateBootstrap(@NotNull Class bootstrapClass, @NotNull JarInJarClassLoader classLoader) throws ReflectiveOperationException; 103 | 104 | /** 105 | * The fully qualified class name for the bootstrap class, this class usually extends {@code AbstractBootstrap}. 106 | * @return the bootstrap class name 107 | */ 108 | @NotNull 109 | String getBootstrapClassName(); 110 | 111 | /** 112 | * Returns the name of this loader this is used for the jarinjar file name which is stored in the system's temporary file directory. 113 | * @return the name of this loader 114 | */ 115 | @NotNull 116 | String getName(); 117 | 118 | /** 119 | * The {@link URL} to the JarInJar resource that contains the bootstrap. 120 | * @return the url to the JarInJar resource 121 | */ 122 | @NotNull 123 | URL getJarInJarResource(); 124 | } 125 | -------------------------------------------------------------------------------- /runtime/src/test/java/dev/vankka/dependencydownload/LoggerTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2021-2025 Vankka 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package dev.vankka.dependencydownload; 26 | 27 | import dev.vankka.dependencydownload.dependency.Dependency; 28 | import dev.vankka.dependencydownload.logger.Logger; 29 | import org.junit.jupiter.api.Test; 30 | 31 | import java.util.*; 32 | 33 | import static dev.vankka.dependencydownload.Helpers.*; 34 | import static org.junit.jupiter.api.Assertions.*; 35 | 36 | public class LoggerTest { 37 | 38 | @Test 39 | public void fullLoggerTest() { 40 | CountingLogger logger = new CountingLogger(); 41 | List dependencies = Collections.singletonList(REAL_DEPENDENCY); 42 | 43 | DependencyManager dependencyManager = new DependencyManager(PATH_PROVIDER, logger); 44 | dependencyManager.addDependencies(dependencies); 45 | 46 | logger.expectNothing(); 47 | 48 | dependencyManager.downloadAll(null, Collections.singletonList(REAL_REPOSITORY)); 49 | logger.expectExactly("downloadStart", 1); 50 | // May not be downloaded always (static cache directory) 51 | logger.expectBetween("downloadDependency", 0, 1); 52 | logger.expectBetween("downloadSuccess", 0, 1); 53 | logger.expectExactly("downloadEnd", 1); 54 | logger.expectNothing(); 55 | 56 | dependencyManager.relocateAll(null); 57 | logger.expectExactly("relocateStart", 1); 58 | logger.expectExactly("relocateDependency", dependencies); 59 | logger.expectExactly("relocateSuccess", dependencies); 60 | logger.expectExactly("relocateEnd", 1); 61 | logger.expectNothing(); 62 | 63 | dependencyManager.loadAll(null, url -> {}); 64 | logger.expectExactly("loadStart", 1); 65 | logger.expectExactly("loadDependency", dependencies); 66 | logger.expectExactly("loadSuccess", dependencies); 67 | logger.expectExactly("loadEnd", 1); 68 | logger.expectNothing(); 69 | } 70 | 71 | private static class CountingLogger implements Logger { 72 | private final Map> received = new HashMap<>(); 73 | 74 | public void expectExactly(String method, List dependencies) { 75 | List actual = received.remove(method); 76 | assertEquals(dependencies, actual, method); 77 | } 78 | 79 | public void expectExactly(String method, int amount) { 80 | List actual = received.remove(method); 81 | assertEquals(amount, actual.size(), method); 82 | } 83 | 84 | public void expectBetween(String method, int min, int max) { 85 | List actual = received.remove(method); 86 | int amount = actual != null ? actual.size() : 0; 87 | assertTrue(amount >= min && amount <= max, method + " >= " + min + " && <= " + max); 88 | } 89 | 90 | public void expectNothing() { 91 | assertEquals(0, received.size(), "Expected nothing else, still had keys: " + received.keySet()); 92 | } 93 | 94 | private void receive(String method) { 95 | receive(method, null); 96 | } 97 | 98 | private void receive(String method, Dependency dependency) { 99 | received.computeIfAbsent(method, k -> new ArrayList<>()).add(dependency); 100 | } 101 | 102 | @Override 103 | public void downloadStart() { 104 | receive("downloadStart"); 105 | } 106 | 107 | @Override 108 | public void downloadDependency(Dependency dependency) { 109 | receive("downloadDependency", dependency); 110 | } 111 | 112 | @Override 113 | public void downloadSuccess(Dependency dependency) { 114 | receive("downloadSuccess", dependency); 115 | } 116 | 117 | @Override 118 | public void downloadFailed(Dependency dependency, Throwable throwable) { 119 | receive("downloadFailed", dependency); 120 | } 121 | 122 | @Override 123 | public void downloadEnd() { 124 | receive("downloadEnd"); 125 | } 126 | 127 | @Override 128 | public void relocateStart() { 129 | receive("relocateStart"); 130 | } 131 | 132 | @Override 133 | public void relocateDependency(Dependency dependency) { 134 | receive("relocateDependency", dependency); 135 | } 136 | 137 | @Override 138 | public void relocateSuccess(Dependency dependency) { 139 | receive("relocateSuccess", dependency); 140 | } 141 | 142 | @Override 143 | public void relocateFailed(Dependency dependency, Throwable throwable) { 144 | fail("relocateFailed", throwable); 145 | } 146 | 147 | @Override 148 | public void relocateEnd() { 149 | receive("relocateEnd"); 150 | } 151 | 152 | @Override 153 | public void loadStart() { 154 | receive("loadStart"); 155 | } 156 | 157 | @Override 158 | public void loadDependency(Dependency dependency) { 159 | receive("loadDependency", dependency); 160 | } 161 | 162 | @Override 163 | public void loadSuccess(Dependency dependency) { 164 | receive("loadSuccess", dependency); 165 | } 166 | 167 | @Override 168 | public void loadFailed(Dependency dependency, Throwable throwable) { 169 | receive("loadFailed", dependency); 170 | } 171 | 172 | @Override 173 | public void loadEnd() { 174 | receive("loadEnd"); 175 | } 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /runtime/src/test/java/dev/vankka/dependencydownload/DependencyMangerTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2021-2025 Vankka 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package dev.vankka.dependencydownload; 26 | 27 | import org.junit.jupiter.api.Test; 28 | 29 | import java.io.IOException; 30 | import java.nio.file.Files; 31 | import java.nio.file.Path; 32 | import java.util.Collections; 33 | import java.util.concurrent.CompletableFuture; 34 | import java.util.concurrent.ExecutionException; 35 | import java.util.concurrent.atomic.AtomicInteger; 36 | 37 | import static dev.vankka.dependencydownload.Helpers.*; 38 | import static org.junit.jupiter.api.Assertions.*; 39 | 40 | public class DependencyMangerTest { 41 | 42 | @Test 43 | public void duplicationTest() { 44 | DependencyManager dependencyManager = new DependencyManager(PATH_PROVIDER); 45 | 46 | assertEquals(0, dependencyManager.getDependencies().size()); 47 | dependencyManager.addDependencies(FAKE_DEPENDENCY_1); 48 | assertEquals(1, dependencyManager.getDependencies().size()); 49 | 50 | // Expected, only ApplicationDependencyManager handles duplicates 51 | dependencyManager.addDependencies(FAKE_DEPENDENCY_1); 52 | assertEquals(2, dependencyManager.getDependencies().size()); 53 | } 54 | 55 | @Test 56 | public void addDependenciesTest() { 57 | DependencyManager dependencyManager = new DependencyManager(PATH_PROVIDER); 58 | 59 | assertEquals(0, dependencyManager.getAllPaths(true).size()); 60 | assertEquals(0, dependencyManager.getDependencies().size()); 61 | 62 | dependencyManager.addDependencies(FAKE_DEPENDENCY_1); 63 | assertEquals(1, dependencyManager.getDependencies().size()); 64 | assertEquals(1, dependencyManager.getPaths(false).size()); 65 | assertEquals(1, dependencyManager.getPaths(true).size()); 66 | assertEquals(2, dependencyManager.getAllPaths(true).size()); 67 | 68 | dependencyManager.addDependencies(FAKE_DEPENDENCY_2); 69 | assertEquals(2, dependencyManager.getDependencies().size()); 70 | assertEquals(2, dependencyManager.getPaths(false).size()); 71 | assertEquals(2, dependencyManager.getPaths(true).size()); 72 | assertEquals(4, dependencyManager.getAllPaths(true).size()); 73 | } 74 | 75 | @Test 76 | public void test() { 77 | DependencyManager dependencyManager = new DependencyManager(PATH_PROVIDER); 78 | dependencyManager.addDependencies(REAL_DEPENDENCY); 79 | dependencyManager.addRelocations(REAL_RELOCATION); 80 | 81 | assertEquals(Collections.singletonList(REAL_DEPENDENCY), dependencyManager.getDependencies(), "dependencies match"); 82 | assertEquals(Collections.singletonList(REAL_RELOCATION), dependencyManager.getRelocations(), "relocations match"); 83 | 84 | assertThrows(IllegalArgumentException.class, () -> dependencyManager.downloadAll(null, Collections.emptyList())); 85 | assertThrows(IllegalStateException.class, () -> dependencyManager.relocateAll(null)); 86 | assertThrows(IllegalStateException.class, () -> dependencyManager.loadAll(null, url -> fail("Load classpath appender called"))); 87 | 88 | CompletableFuture download = dependencyManager.downloadAll(null, Collections.singletonList(REAL_REPOSITORY)); 89 | assertNotNull(download, "download future is not null"); 90 | assertTrue(download.isDone(), "download future is done"); 91 | assertFalse(download.isCompletedExceptionally(), "download did not fail"); 92 | assertThrows(IllegalStateException.class, () -> dependencyManager.downloadAll(null, Collections.singletonList(REAL_REPOSITORY))); 93 | 94 | CompletableFuture relocate = dependencyManager.relocateAll(null); 95 | assertNotNull(relocate, "relocate future is not null"); 96 | assertTrue(relocate.isDone(), "relocate future is done"); 97 | assertFalse(relocate.isCompletedExceptionally(), "relocate did not fail"); 98 | assertThrows(IllegalStateException.class, () -> dependencyManager.relocateAll(null)); 99 | 100 | AtomicInteger calledTimes = new AtomicInteger(0); 101 | CompletableFuture load = dependencyManager.loadAll(null, url -> calledTimes.incrementAndGet()); 102 | assertNotNull(load, "load future is not null"); 103 | assertTrue(load.isDone(), "load future is done"); 104 | assertFalse(load.isCompletedExceptionally(), "load did not fail"); 105 | assertEquals(1, calledTimes.get(), "ClasspathAppender called only once"); 106 | assertTrue(dependencyManager.isLoaded(), "DependencyManager.isLoaded"); 107 | assertThrows(IllegalStateException.class, () -> dependencyManager.loadAll(null, url -> fail("Load classpath appender called"))); 108 | } 109 | 110 | @Test 111 | public void cleanupTest() throws IOException { 112 | DependencyManager dependencyManager = new DependencyManager(PATH_PROVIDER_FOR_CLEANUP); 113 | dependencyManager.addDependencies(FAKE_DEPENDENCY_1); // only 1 114 | 115 | Path path1 = dependencyManager.getPathForDependency(FAKE_DEPENDENCY_1, false); 116 | Files.createDirectories(path1.getParent()); 117 | Files.write(path1, new byte[0]); 118 | assertTrue(Files.exists(path1), "file 1 exists before cleanup"); 119 | 120 | Path path1relocated = dependencyManager.getPathForDependency(FAKE_DEPENDENCY_1, true); 121 | Files.write(path1relocated, new byte[0]); 122 | assertTrue(Files.exists(path1relocated), "relocated file 1 exists before cleanup"); 123 | 124 | Path path2 = dependencyManager.getPathForDependency(FAKE_DEPENDENCY_2, false); 125 | Files.write(path2, new byte[0]); 126 | assertTrue(Files.exists(path2), "file 2 exists before cleanup"); 127 | 128 | Path path2relocated = dependencyManager.getPathForDependency(FAKE_DEPENDENCY_2, true); 129 | Files.write(path2relocated, new byte[0]); 130 | assertTrue(Files.exists(path2relocated), "relocated file 2 exists before cleanup"); 131 | 132 | dependencyManager.cleanupCacheDirectory(); 133 | assertTrue(Files.exists(path1), "file 1 exists after cleanup"); 134 | assertFalse(Files.exists(path2), "file 2 no longer exists after cleanup"); 135 | assertTrue(Files.exists(path1relocated), "relocated file 1 exists after cleanup"); 136 | assertFalse(Files.exists(path2relocated), "relocated file 2 no longer exists after cleanup"); 137 | } 138 | 139 | @Test 140 | public void downloadFailTest() { 141 | DependencyManager dependencyManager = new DependencyManager(PATH_PROVIDER); 142 | dependencyManager.addDependencies(FAKE_DEPENDENCY_1); 143 | 144 | CompletableFuture future = dependencyManager.downloadAll(null, Collections.singletonList(FAKE_REPOSITORY)); 145 | assertTrue(future.isDone(), "download future is done"); 146 | assertTrue(future.isCompletedExceptionally(), "download future did fail"); 147 | try { 148 | future.get(); 149 | } catch (ExecutionException e) { 150 | assertThrows(RuntimeException.class, () -> { 151 | throw e.getCause(); 152 | }); 153 | } catch (InterruptedException e) { 154 | fail("Interrupted", e); 155 | } 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original 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 POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | # This is normally unused 84 | # shellcheck disable=SC2034 85 | APP_BASE_NAME=${0##*/} 86 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 87 | APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit 88 | 89 | # Use the maximum available, or set MAX_FD != -1 to use that value. 90 | MAX_FD=maximum 91 | 92 | warn () { 93 | echo "$*" 94 | } >&2 95 | 96 | die () { 97 | echo 98 | echo "$*" 99 | echo 100 | exit 1 101 | } >&2 102 | 103 | # OS specific support (must be 'true' or 'false'). 104 | cygwin=false 105 | msys=false 106 | darwin=false 107 | nonstop=false 108 | case "$( uname )" in #( 109 | CYGWIN* ) cygwin=true ;; #( 110 | Darwin* ) darwin=true ;; #( 111 | MSYS* | MINGW* ) msys=true ;; #( 112 | NONSTOP* ) nonstop=true ;; 113 | esac 114 | 115 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 116 | 117 | 118 | # Determine the Java command to use to start the JVM. 119 | if [ -n "$JAVA_HOME" ] ; then 120 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 121 | # IBM's JDK on AIX uses strange locations for the executables 122 | JAVACMD=$JAVA_HOME/jre/sh/java 123 | else 124 | JAVACMD=$JAVA_HOME/bin/java 125 | fi 126 | if [ ! -x "$JAVACMD" ] ; then 127 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 128 | 129 | Please set the JAVA_HOME variable in your environment to match the 130 | location of your Java installation." 131 | fi 132 | else 133 | JAVACMD=java 134 | if ! command -v java >/dev/null 2>&1 135 | then 136 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | fi 142 | 143 | # Increase the maximum file descriptors if we can. 144 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 145 | case $MAX_FD in #( 146 | max*) 147 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 148 | # shellcheck disable=SC2039,SC3045 149 | MAX_FD=$( ulimit -H -n ) || 150 | warn "Could not query maximum file descriptor limit" 151 | esac 152 | case $MAX_FD in #( 153 | '' | soft) :;; #( 154 | *) 155 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 156 | # shellcheck disable=SC2039,SC3045 157 | ulimit -n "$MAX_FD" || 158 | warn "Could not set maximum file descriptor limit to $MAX_FD" 159 | esac 160 | fi 161 | 162 | # Collect all arguments for the java command, stacking in reverse order: 163 | # * args from the command line 164 | # * the main class name 165 | # * -classpath 166 | # * -D...appname settings 167 | # * --module-path (only if needed) 168 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 169 | 170 | # For Cygwin or MSYS, switch paths to Windows format before running java 171 | if "$cygwin" || "$msys" ; then 172 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 173 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 174 | 175 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 176 | 177 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 178 | for arg do 179 | if 180 | case $arg in #( 181 | -*) false ;; # don't mess with options #( 182 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 183 | [ -e "$t" ] ;; #( 184 | *) false ;; 185 | esac 186 | then 187 | arg=$( cygpath --path --ignore --mixed "$arg" ) 188 | fi 189 | # Roll the args list around exactly as many times as the number of 190 | # args, so each arg winds up back in the position where it started, but 191 | # possibly modified. 192 | # 193 | # NB: a `for` loop captures its iteration list before it begins, so 194 | # changing the positional parameters here affects neither the number of 195 | # iterations, nor the values presented in `arg`. 196 | shift # remove old arg 197 | set -- "$@" "$arg" # push replacement arg 198 | done 199 | fi 200 | 201 | 202 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 203 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 204 | 205 | # Collect all arguments for the java command: 206 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 207 | # and any embedded shellness will be escaped. 208 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 209 | # treated as '${Hostname}' itself on the command line. 210 | 211 | set -- \ 212 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 213 | -classpath "$CLASSPATH" \ 214 | org.gradle.wrapper.GradleWrapperMain \ 215 | "$@" 216 | 217 | # Stop when "xargs" is not available. 218 | if ! command -v xargs >/dev/null 2>&1 219 | then 220 | die "xargs is not available" 221 | fi 222 | 223 | # Use "xargs" to parse quoted args. 224 | # 225 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 226 | # 227 | # In Bash we could simply go: 228 | # 229 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 230 | # set -- "${ARGS[@]}" "$@" 231 | # 232 | # but POSIX shell has neither arrays nor command substitution, so instead we 233 | # post-process each arg (as a line of input to sed) to backslash-escape any 234 | # character that might be a shell metacharacter, then use eval to reverse 235 | # that process (while maintaining the separation between arguments), and wrap 236 | # the whole thing up as a single "set" statement. 237 | # 238 | # This will of course break if any of these variables contains a newline or 239 | # an unmatched quote. 240 | # 241 | 242 | eval "set -- $( 243 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 244 | xargs -n1 | 245 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 246 | tr '\n' ' ' 247 | )" '"$@"' 248 | 249 | exec "$JAVACMD" "$@" 250 | -------------------------------------------------------------------------------- /runtime/src/main/java/dev/vankka/dependencydownload/resource/DependencyDownloadResource.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2021-2025 Vankka 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package dev.vankka.dependencydownload.resource; 26 | 27 | import dev.vankka.dependencydownload.dependency.Dependency; 28 | import dev.vankka.dependencydownload.dependency.MavenDependency; 29 | import dev.vankka.dependencydownload.relocation.Relocation; 30 | import org.intellij.lang.annotations.Subst; 31 | import org.jetbrains.annotations.NotNull; 32 | 33 | import java.io.BufferedReader; 34 | import java.io.IOException; 35 | import java.io.InputStream; 36 | import java.io.InputStreamReader; 37 | import java.net.URL; 38 | import java.util.*; 39 | import java.util.stream.Collectors; 40 | 41 | /** 42 | * An object representation of the resource generated by the Gradle plugin. 43 | */ 44 | public class DependencyDownloadResource { 45 | 46 | /** 47 | * Parse into a {@link DependencyDownloadResource} using the URL to the resource. 48 | * 49 | * @param resourceURL the url to the resource generated by the Gradle plugin 50 | * @return a new parsed {@link DependencyDownloadResource} 51 | * @throws IOException if reading the resource fails 52 | */ 53 | public static DependencyDownloadResource parse(@NotNull URL resourceURL) throws IOException { 54 | List lines; 55 | try (InputStream inputStream = resourceURL.openStream()) { 56 | lines = new BufferedReader(new InputStreamReader(inputStream)) 57 | .lines() 58 | .collect(Collectors.toList()); 59 | } 60 | return parse(lines); 61 | } 62 | 63 | /** 64 | * Parse into a {@link DependencyDownloadResource} using the resource file content. 65 | * 66 | * @param resourceFileContent the content of the resource generated by the Gradle plugin 67 | * @return a new parsed {@link DependencyDownloadResource} 68 | */ 69 | public static DependencyDownloadResource parse(@NotNull String resourceFileContent) { 70 | return parse(Arrays.asList(resourceFileContent.split("\n"))); 71 | } 72 | 73 | /** 74 | * Parse into a {@link DependencyDownloadResource} using the resource file content. 75 | * 76 | * @param resourceLines the lines of the resource generated by the Gradle plugin 77 | * @return a new parsed {@link DependencyDownloadResource} 78 | */ 79 | public static DependencyDownloadResource parse(List resourceLines) { 80 | return new DependencyDownloadResource(resourceLines); 81 | } 82 | 83 | private final List dependencies = new ArrayList<>(); 84 | private final List relocations = new ArrayList<>(); 85 | 86 | private DependencyDownloadResource(@NotNull List resourceLines) { 87 | readFile(resourceLines); 88 | } 89 | 90 | private void readFile(List lines) { 91 | String hashingAlgorithm = null; 92 | 93 | int relocationStep = -1; 94 | String pattern = null; 95 | String replacement = null; 96 | Set include = null; 97 | for (String line : lines) { 98 | line = line.trim(); 99 | if (line.isEmpty()) { 100 | continue; 101 | } 102 | 103 | if (line.startsWith("===ALGORITHM")) { 104 | String[] algorithmParts = line.split(" "); 105 | if (algorithmParts.length != 2) { 106 | throw new IllegalArgumentException("Resource format is invalid: hashing algorithm: " + line); 107 | } 108 | hashingAlgorithm = algorithmParts[1]; 109 | continue; 110 | } else if (hashingAlgorithm == null) { 111 | throw new IllegalArgumentException("Resource format is invalid: no hashing algorithm"); 112 | } else if (line.startsWith("===RELOCATIONS") && relocationStep == -1) { 113 | relocationStep = 0; 114 | continue; 115 | } else if (relocationStep != -1) { 116 | switch (relocationStep) { 117 | case 0: 118 | pattern = line; 119 | break; 120 | case 1: 121 | replacement = line; 122 | break; 123 | case 2: 124 | case 3: 125 | if (!line.startsWith("[") || !line.endsWith("]")) { 126 | throw new IllegalArgumentException("Resource format is invalid: expecting a includes/excludes: " + line); 127 | } 128 | line = line.substring(1, line.length() - 1); 129 | 130 | Set values; 131 | if (!line.trim().isEmpty()) { 132 | values = new HashSet<>(Arrays.asList(line.split(","))); 133 | } else { 134 | values = Collections.emptySet(); 135 | } 136 | 137 | if (relocationStep == 2) { 138 | include = values; 139 | } else { 140 | if (replacement == null) { 141 | throw new IllegalStateException("Replacement may not be null"); 142 | } 143 | relocations.add(new Relocation(pattern, replacement, include, values)); 144 | include = null; 145 | relocationStep = 0; 146 | continue; 147 | } 148 | } 149 | 150 | relocationStep++; 151 | continue; 152 | } 153 | 154 | String[] parts = line.split(" "); 155 | if (parts.length != 2) { 156 | throw new IllegalArgumentException("Resource format is invalid: invalid dependency: " + line); 157 | } 158 | String maven = parts[0]; 159 | String hash = parts[1]; 160 | 161 | String[] mavenParts = maven.split(":"); 162 | int partCount = mavenParts.length; 163 | if (partCount < 3 || partCount > 5) { 164 | throw new IllegalArgumentException("Resource format is invalid: invalid dependency GAV: " + maven + " (" + partCount + ")"); 165 | } 166 | 167 | @Subst("dev.vankka") String group = mavenParts[0]; 168 | @Subst("dependencydownload-runtime") String artifact = mavenParts[1]; 169 | @Subst("1.0.0") String version = mavenParts[2]; 170 | @Subst("20240101.120000-1") String snapshotTimestamp; 171 | @Subst("shaded") String classifier; 172 | if (version.endsWith("-SNAPSHOT")) { 173 | snapshotTimestamp = mavenParts[3]; 174 | if (partCount == 5) { 175 | classifier = mavenParts[4]; 176 | } else { 177 | classifier = null; 178 | } 179 | } else { 180 | snapshotTimestamp = null; 181 | if (partCount == 4) { 182 | classifier = mavenParts[3]; 183 | } else { 184 | classifier = null; 185 | } 186 | } 187 | 188 | Dependency dependency; 189 | if (snapshotTimestamp == null) { 190 | dependency = new MavenDependency( 191 | group, 192 | artifact, 193 | version, 194 | classifier, 195 | hash, 196 | hashingAlgorithm 197 | ); 198 | } else { 199 | dependency = new MavenDependency( 200 | group, 201 | artifact, 202 | version, 203 | classifier, 204 | snapshotTimestamp, 205 | hash, 206 | hashingAlgorithm 207 | ); 208 | } 209 | 210 | dependencies.add(dependency); 211 | } 212 | if (relocationStep > 1) { // has pattern & replacement w/o include and/or exclude 213 | relocations.add(new Relocation(pattern, replacement, include, null)); 214 | } else if (relocationStep == 1) { 215 | throw new IllegalArgumentException("Resource format is invalid: relocation missing replacement"); 216 | } 217 | } 218 | 219 | /** 220 | * Gets the dependencies defined in the resource. 221 | * @return the dependencies defined in this resource 222 | */ 223 | public List getDependencies() { 224 | return dependencies; 225 | } 226 | 227 | /** 228 | * Gets the relocations defined in the resource. 229 | * @return the relocations defined in this resource 230 | */ 231 | public List getRelocations() { 232 | return relocations; 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /runtime/src/main/java/dev/vankka/dependencydownload/ApplicationDependencyManager.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2021-2025 Vankka 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package dev.vankka.dependencydownload; 26 | 27 | import dev.vankka.dependencydownload.dependency.Dependency; 28 | import dev.vankka.dependencydownload.logger.Logger; 29 | import dev.vankka.dependencydownload.path.CleanupPathProvider; 30 | import dev.vankka.dependencydownload.path.DependencyPathProvider; 31 | import dev.vankka.dependencydownload.relocation.Relocation; 32 | import dev.vankka.dependencydownload.resource.DependencyDownloadResource; 33 | import org.jetbrains.annotations.CheckReturnValue; 34 | import org.jetbrains.annotations.NotNull; 35 | 36 | import java.io.IOException; 37 | import java.nio.file.Path; 38 | import java.util.*; 39 | 40 | /** 41 | * An application level dependency manager to prevent loading in the same dependency multiple times. 42 | */ 43 | @SuppressWarnings("unused") // API 44 | public class ApplicationDependencyManager { 45 | 46 | private final DependencyManager dependencyManager; 47 | 48 | /** 49 | * Creates a {@link ApplicationDependencyManager}. 50 | * @param dependencyPathProvider the dependencyPathProvider used for downloaded and relocated dependencies 51 | */ 52 | public ApplicationDependencyManager(@NotNull DependencyPathProvider dependencyPathProvider) { 53 | this(dependencyPathProvider, Logger.NOOP); 54 | } 55 | /** 56 | * Creates a {@link ApplicationDependencyManager}. 57 | * @param dependencyPathProvider the dependencyPathProvider used for downloaded and relocated dependencies 58 | * @param logger the logger to use 59 | */ 60 | public ApplicationDependencyManager(@NotNull DependencyPathProvider dependencyPathProvider, Logger logger) { 61 | this.dependencyManager = new DependencyManager(dependencyPathProvider, logger); 62 | } 63 | 64 | /** 65 | * Gets the dependency path provider for this {@link ApplicationDependencyManager}. 66 | * @return the instance of {@link DependencyPathProvider} 67 | */ 68 | @NotNull 69 | public DependencyPathProvider getDependencyPathProvider() { 70 | return dependencyManager.getDependencyPathProvider(); 71 | } 72 | 73 | /** 74 | * Gets the logger being used by this {@link ApplicationDependencyManager}. 75 | * @return the instance of {@link Logger} being used 76 | */ 77 | @NotNull 78 | public Logger getLogger() { 79 | return dependencyManager.getLogger(); 80 | } 81 | 82 | /** 83 | * Adds the provided relocations to this {@link ApplicationDependencyManager}, 84 | * they will be used in all {@link DependencyManager}s created by this manager after being added. 85 | * @param relocations the relocations to add 86 | * @see #addRelocations(Collection) 87 | */ 88 | @NotNull 89 | public ApplicationDependencyManager addRelocations(@NotNull Relocation... relocations) { 90 | return addRelocations(Arrays.asList(relocations)); 91 | } 92 | 93 | /** 94 | * Adds the provided relocations to this {@link ApplicationDependencyManager}, 95 | * they will be used in all {@link DependencyManager}s created by this manager after being added. 96 | * @param relocations the relocations to add 97 | * @see #addRelocations(Relocation...) 98 | */ 99 | @NotNull 100 | public ApplicationDependencyManager addRelocations(@NotNull Collection relocations) { 101 | synchronized (dependencyManager) { 102 | dependencyManager.addRelocations(relocations); 103 | } 104 | return this; 105 | } 106 | 107 | /** 108 | * Includes the dependencies and relocations from the {@link DependencyDownloadResource} provided as an argument. 109 | *

110 | * The returned {@link DependencyManager} will only include dependencies that have not been downloaded yet, 111 | * and will include all the relocations from this manager. 112 | * 113 | * @param resource the resource to get dependencies and relocations from 114 | * @return the {@link DependencyManager} to load in the dependencies 115 | */ 116 | @CheckReturnValue 117 | @NotNull 118 | public DependencyManager includeResource(@NotNull DependencyDownloadResource resource) { 119 | addRelocations(resource.getRelocations()); 120 | return include(resource.getDependencies()); 121 | } 122 | 123 | /** 124 | * Includes the provided dependencies. 125 | *

126 | * The returned {@link DependencyManager} will only include dependencies that have not been downloaded yet, 127 | * and will include all the relocations from this manager. 128 | * 129 | * @param dependencies the dependencies to include 130 | * @return the {@link DependencyManager} to load in the dependencies 131 | */ 132 | @CheckReturnValue 133 | @NotNull 134 | public DependencyManager include(@NotNull Dependency... dependencies) { 135 | return include(Arrays.asList(dependencies)); 136 | } 137 | 138 | /** 139 | * Includes the provided dependencies. 140 | *

141 | * The returned {@link DependencyManager} will only include dependencies that have not been downloaded yet, 142 | * and will include all the relocations from this manager. 143 | * 144 | * @param dependencies the dependencies to include 145 | * @return a new {@link DependencyManager} to load in the dependencies 146 | */ 147 | @CheckReturnValue 148 | @NotNull 149 | public DependencyManager include(@NotNull Collection dependencies) { 150 | dependencies = addMissingDependencies(dependencies); 151 | 152 | DependencyManager dependencyManager = new DependencyManager(getDependencyPathProvider(), getLogger()); 153 | dependencyManager.addDependencies(dependencies); 154 | synchronized (this.dependencyManager) { 155 | dependencyManager.addRelocations(this.dependencyManager.getRelocations()); 156 | } 157 | return dependencyManager; 158 | } 159 | 160 | /** 161 | * Includes the dependencies and relocations from the provided {@link DependencyManager}, 162 | * the {@link DependencyPathProvider} and {@link Logger} will be preserved from the provided {@link DependencyManager}. 163 | *

164 | * The returned {@link DependencyManager} will only include dependencies that have not been downloaded yet, 165 | * and will include all the relocations from this manager (including ones from the provided {@link DependencyManager}). 166 | * 167 | * @param manager the manager to get dependencies and relocations from 168 | * @return a new {@link DependencyManager} to load in the dependencies, or if the provided manager has already been loaded, it will be returned instead 169 | */ 170 | @NotNull 171 | public DependencyManager include(@NotNull DependencyManager manager) { 172 | addRelocations(manager.getRelocations()); 173 | List dependencies = addMissingDependencies(manager.getDependencies()); 174 | if (manager.isLoaded()) { 175 | return manager; 176 | } 177 | 178 | DependencyManager dependencyManager = new DependencyManager(manager.getDependencyPathProvider(), manager.getLogger()); 179 | dependencyManager.addDependencies(dependencies); 180 | synchronized (this.dependencyManager) { 181 | dependencyManager.addRelocations(this.dependencyManager.getRelocations()); 182 | } 183 | return dependencyManager; 184 | } 185 | 186 | /** 187 | * Gets the {@link Path} where the given {@link Dependency} will be stored once downloaded. 188 | * 189 | * @param dependency the dependency. 190 | * @param relocated if the path should be for the relocated or unrelocated file of the Dependency 191 | * @return the path for the dependency 192 | */ 193 | @NotNull 194 | public Path getPathForDependency(@NotNull Dependency dependency, boolean relocated) { 195 | synchronized (dependencyManager) { 196 | return dependencyManager.getPathForDependency(dependency, relocated); 197 | } 198 | } 199 | 200 | /** 201 | * Gets {@link Path}s to all {@link Dependency Dependencies} in this {@link ApplicationDependencyManager}, 202 | * optionally also including the relocated paths if {@code includeRelocated} is set to {@code true}. 203 | * @param relocated relocated paths, otherwise unrelocated paths 204 | * @return paths to all dependencies, original or relocated 205 | * @see #getPathForDependency(Dependency, boolean) 206 | */ 207 | @NotNull 208 | public Set getPaths(boolean relocated) { 209 | synchronized (dependencyManager) { 210 | return dependencyManager.getPaths(relocated); 211 | } 212 | } 213 | 214 | /** 215 | * Gets {@link Path}s to all {@link Dependency Dependencies} in this {@link ApplicationDependencyManager}, 216 | * optionally also including the relocated paths if {@code includeRelocated} is set to {@code true}. 217 | * @param includeRelocated if relocated paths should also be included 218 | * @return paths to all dependencies (and optionally relocated dependencies) 219 | * @see #getPathForDependency(Dependency, boolean) 220 | */ 221 | @NotNull 222 | public Set getAllPaths(boolean includeRelocated) { 223 | synchronized (dependencyManager) { 224 | return dependencyManager.getAllPaths(includeRelocated); 225 | } 226 | } 227 | 228 | /** 229 | * Removes files that are not known dependencies of this {@link DependencyManager} from {@link CleanupPathProvider#getPathsForAllStoredDependencies()} implementation. 230 | * 231 | * This only accounts for dependencies that are included in this {@link DependencyManager} instance! 232 | * 233 | * 234 | * @throws IOException if listing files in the cache directory or deleting files in it fails 235 | * @throws IllegalStateException if this DependencyManager's dependencyPathProvider isn't an instance of {@link CleanupPathProvider} 236 | * @see CleanupPathProvider 237 | */ 238 | public void cleanupCacheDirectory() throws IOException, IllegalStateException { 239 | synchronized (dependencyManager) { 240 | dependencyManager.cleanupCacheDirectory(); 241 | } 242 | } 243 | 244 | private List addMissingDependencies(Collection old) { 245 | List missingDependencies = new ArrayList<>(old.size()); 246 | for (Dependency dependency : old) { 247 | String group = dependency.getGroupId(); 248 | String artifact = dependency.getArtifactId(); 249 | 250 | synchronized (dependencyManager) { 251 | // Check that there is no dependency with the same group + artifact id 252 | boolean noMatch = dependencyManager.getDependencies().stream() 253 | .noneMatch(dep -> dep.getGroupId().equals(group) 254 | && dep.getArtifactId().equals(artifact)); 255 | 256 | if (noMatch) { 257 | missingDependencies.add(dependency); 258 | dependencyManager.addDependencies(dependency); 259 | } 260 | } 261 | } 262 | return missingDependencies; 263 | } 264 | 265 | } 266 | -------------------------------------------------------------------------------- /gradle-plugin/src/main/java/dev/vankka/dependencydownload/task/GenerateDependencyDownloadResourceTask.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2021-2025 Vankka 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package dev.vankka.dependencydownload.task; 26 | 27 | import dev.vankka.dependencydownload.Dependency; 28 | import dev.vankka.dependencydownload.DependencyDownloadGradlePlugin; 29 | import dev.vankka.dependencydownload.common.util.HashUtil; 30 | import dev.vankka.dependencydownload.inputs.Relocation; 31 | import dev.vankka.dependencydownload.inputs.ResourceSplittingStrategy; 32 | import org.gradle.api.Action; 33 | import org.gradle.api.DefaultTask; 34 | import org.gradle.api.Task; 35 | import org.gradle.api.artifacts.Configuration; 36 | import org.gradle.api.artifacts.ResolvedArtifact; 37 | import org.gradle.api.artifacts.ResolvedDependency; 38 | import org.gradle.api.artifacts.component.ComponentArtifactIdentifier; 39 | import org.gradle.api.artifacts.component.ComponentIdentifier; 40 | import org.gradle.api.file.RegularFileProperty; 41 | import org.gradle.api.internal.artifacts.repositories.resolver.MavenUniqueSnapshotComponentIdentifier; 42 | import org.gradle.api.logging.Logger; 43 | import org.gradle.api.model.ObjectFactory; 44 | import org.gradle.api.plugins.JavaPluginExtension; 45 | import org.gradle.api.provider.Property; 46 | import org.gradle.api.tasks.Optional; 47 | import org.gradle.api.tasks.*; 48 | 49 | import javax.inject.Inject; 50 | import java.io.File; 51 | import java.io.FileWriter; 52 | import java.io.IOException; 53 | import java.nio.file.Files; 54 | import java.nio.file.Path; 55 | import java.security.MessageDigest; 56 | import java.security.NoSuchAlgorithmException; 57 | import java.util.*; 58 | 59 | public abstract class GenerateDependencyDownloadResourceTask extends DefaultTask { 60 | 61 | @Classpath 62 | public abstract Property getConfiguration(); 63 | 64 | @OutputDirectory 65 | public abstract RegularFileProperty getFileLocation(); 66 | 67 | @Input 68 | @Optional 69 | public abstract Property getFile(); 70 | 71 | @Input 72 | public abstract Property getIncludeShadowJarRelocations(); 73 | 74 | @Input 75 | public abstract Property getHashingAlgorithm(); 76 | 77 | @Input 78 | public abstract Property getResourceSplittingStrategy(); 79 | 80 | private final List relocations = new ArrayList<>(); 81 | 82 | @Inject 83 | public GenerateDependencyDownloadResourceTask(ObjectFactory factory) { 84 | getConfiguration().convention( 85 | getProject().getConfigurations().getByName(DependencyDownloadGradlePlugin.BASE_CONFIGURATION_NAME)); 86 | getFileLocation().convention(getConfiguration().flatMap(conf -> factory.fileProperty().fileValue(getResourceDirectory(conf)))); 87 | getIncludeShadowJarRelocations().convention(true); 88 | getHashingAlgorithm().convention("SHA-256"); 89 | getResourceSplittingStrategy().convention(ResourceSplittingStrategy.SINGLE_FILE); 90 | } 91 | 92 | // 93 | // Relocations 94 | // 95 | 96 | @SuppressWarnings("unused") // API 97 | public GenerateDependencyDownloadResourceTask relocate(String pattern, String destination) { 98 | return relocate(pattern, destination, null); 99 | } 100 | 101 | public GenerateDependencyDownloadResourceTask relocate(String pattern, String destination, Action configure) { 102 | Relocation relocation = new Relocation(pattern, destination, new ArrayList<>(), new ArrayList<>()); 103 | addRelocator(relocation, configure); 104 | return this; 105 | } 106 | 107 | @SuppressWarnings("unused") // API 108 | public GenerateDependencyDownloadResourceTask relocate(Relocation relocation) { 109 | addRelocator(relocation, null); 110 | return this; 111 | } 112 | 113 | @SuppressWarnings("unused") // API 114 | public GenerateDependencyDownloadResourceTask relocate(Class relocationClass) 115 | throws ReflectiveOperationException { 116 | return relocate(relocationClass, null); 117 | } 118 | 119 | public GenerateDependencyDownloadResourceTask relocate(Class relocatorClass, Action configure) 120 | throws ReflectiveOperationException { 121 | R relocator = relocatorClass.getDeclaredConstructor().newInstance(); 122 | addRelocator(relocator, configure); 123 | return this; 124 | } 125 | 126 | private void addRelocator(R relocation, Action configure) { 127 | if (configure != null) { 128 | configure.execute(relocation); 129 | } 130 | 131 | relocations.add(relocation); 132 | } 133 | 134 | // 135 | // Utility methods 136 | // 137 | 138 | public void configuration(Configuration configuration) { 139 | getConfiguration().set(configuration); 140 | getFileLocation().convention(getProject().getObjects().fileProperty().fileValue(getResourceDirectory(configuration))); 141 | } 142 | 143 | private File getResourceDirectory(Configuration configuration) { 144 | JavaPluginExtension javaPluginExtension = getProject().getExtensions().getByType(JavaPluginExtension.class); 145 | SourceSetContainer sourceSets = javaPluginExtension.getSourceSets(); 146 | SourceSet sourceSet = sourceSets.getByName(SourceSet.MAIN_SOURCE_SET_NAME); 147 | SourceSetOutput output = sourceSet.getOutput(); 148 | 149 | // Use existing 150 | String configurationName = configuration.getName(); 151 | for (File dir : output.getDirs()) { 152 | if (dir.getName().equals(configurationName)) { 153 | return dir; 154 | } 155 | } 156 | 157 | // Create new 158 | File buildDirectory = getProject().getLayout().getBuildDirectory().get().getAsFile(); 159 | File generatedResourcesDir = new File(buildDirectory, "generated-resources"); 160 | File dependencyDownloadResources = new File(generatedResourcesDir, configurationName); 161 | Map properties = new HashMap<>(); 162 | properties.put("builtBy", configuration); 163 | output.dir(properties, dependencyDownloadResources); 164 | return dependencyDownloadResources; 165 | } 166 | 167 | // 168 | // Action 169 | // 170 | 171 | @TaskAction 172 | public void run() throws NoSuchAlgorithmException, IOException { 173 | // Get the resources directory for this configuration 174 | Path resourcesDirectory = getResourceDirectory(getConfiguration().get()).toPath(); 175 | Path fileLocation = getFileLocation().get().getAsFile().toPath(); 176 | String hashingAlgorithm = getHashingAlgorithm().get(); 177 | ResourceSplittingStrategy splittingStrategy = getResourceSplittingStrategy().get(); 178 | boolean single = splittingStrategy == ResourceSplittingStrategy.SINGLE_FILE; 179 | boolean topLevel = splittingStrategy == ResourceSplittingStrategy.TOP_LEVEL_DEPENDENCIES; 180 | boolean all = splittingStrategy == ResourceSplittingStrategy.ALL_DEPENDENCIES; 181 | 182 | Property property = getConfiguration(); 183 | Configuration configuration; 184 | if (property.isPresent()) { 185 | configuration = property.get(); 186 | } else { 187 | throw new IllegalArgumentException("configuration must be provided"); 188 | } 189 | 190 | Set processedDependencies = new HashSet<>(); 191 | 192 | List dependencies = single ? new ArrayList<>() : null; 193 | for (Configuration config : getConfigurations(configuration)) { 194 | for (ResolvedDependency resolvedDependency : config.getResolvedConfiguration().getFirstLevelModuleDependencies()) { 195 | if (topLevel) { 196 | dependencies = new ArrayList<>(); 197 | } 198 | for (Dependency dependency : processDependency(resolvedDependency, hashingAlgorithm)) { 199 | if (!processedDependencies.add(dependency)) { 200 | // Only add dependencies once 201 | continue; 202 | } 203 | if (!all) { 204 | dependencies.add(dependency); 205 | continue; 206 | } 207 | 208 | // Make a file for all distinct dependencies 209 | makeFile( 210 | resourcesDirectory, 211 | fileLocation, 212 | hashingAlgorithm, 213 | splittingStrategy, 214 | configuration, 215 | Collections.singletonList(dependency) 216 | ); 217 | } 218 | if (topLevel) { 219 | // Make a file for all top level dependencies 220 | makeFile( 221 | resourcesDirectory, 222 | fileLocation, 223 | hashingAlgorithm, 224 | splittingStrategy, 225 | configuration, 226 | dependencies 227 | ); 228 | } 229 | } 230 | } 231 | 232 | if (single) { 233 | // Make a single file for all dependencies in the configuration 234 | makeFile( 235 | resourcesDirectory, 236 | fileLocation, 237 | hashingAlgorithm, 238 | splittingStrategy, 239 | configuration, 240 | dependencies 241 | ); 242 | } 243 | } 244 | 245 | private void makeFile( 246 | Path resourcesDirectory, 247 | Path fileLocation, 248 | String hashingAlgorithm, 249 | ResourceSplittingStrategy splittingStrategy, 250 | Configuration configuration, 251 | List dependencies 252 | ) throws IOException { 253 | StringJoiner result = new StringJoiner("\n"); 254 | result.add("===ALGORITHM " + hashingAlgorithm); 255 | 256 | dependencies.forEach(dependency -> result.add(dependency.toString())); 257 | 258 | List relocations = new ArrayList<>(); 259 | if (getIncludeShadowJarRelocations().get()) { 260 | getShadowJarRelocations(relocations); 261 | } 262 | 263 | for (Relocation relocation : this.relocations) { 264 | relocations.removeIf(rel -> relocation.getPattern().equals(rel.getPattern())); 265 | relocations.add(relocation); 266 | } 267 | 268 | if (!relocations.isEmpty()) { 269 | result.add("===RELOCATIONS"); 270 | for (Relocation relocation : relocations) { 271 | result.add(relocation.getPattern()); 272 | result.add(relocation.getShadedPattern()); 273 | result.add("[" + String.join(",", relocation.getIncludes()) + "]"); 274 | result.add("[" + String.join(",", relocation.getExcludes()) + "]"); 275 | } 276 | } 277 | 278 | // Save the file 279 | if (!Files.exists(resourcesDirectory)) { 280 | Files.createDirectories(resourcesDirectory); 281 | } 282 | 283 | String file = getFile().getOrElse(""); 284 | if (!file.trim().isEmpty() && !dependencies.isEmpty()) { 285 | Dependency dependency = dependencies.get(0); 286 | file = file 287 | .replace("%group%", dependency.getGroup()) 288 | .replace("%module%", dependency.getModule()) 289 | .replace("%version%", dependency.getVersion()) 290 | .replace("%classifier%", dependency.getClassifier() != null ? dependency.getClassifier() : "") 291 | .replace("%hash%", dependency.getHash()); 292 | } else if (file.trim().isEmpty()) { 293 | switch (splittingStrategy) { 294 | default: 295 | case SINGLE_FILE: 296 | file = configuration.getName() + ".txt"; 297 | break; 298 | case TOP_LEVEL_DEPENDENCIES: 299 | case ALL_DEPENDENCIES: 300 | Dependency dependency = dependencies.get(0); 301 | String classifier = dependency.getClassifier(); 302 | file = dependency.getGroup() + ":" + dependency.getModule() 303 | + (classifier != null ? "-" + classifier : "") + ".txt"; 304 | break; 305 | } 306 | } 307 | 308 | Path dependenciesFile = fileLocation.resolve(file); 309 | if (Files.exists(dependenciesFile)) { 310 | Files.delete(dependenciesFile); 311 | } else { 312 | // Create parent directory if it doesn't exist 313 | Files.createDirectories(dependenciesFile.getParent()); 314 | } 315 | Files.createFile(dependenciesFile); 316 | 317 | try (FileWriter writer = new FileWriter(dependenciesFile.toFile())) { 318 | writer.append(result.toString()); 319 | } 320 | } 321 | 322 | private Set getConfigurations(Configuration configuration) { 323 | Set configurations = new HashSet<>(); 324 | configurations.add(configuration); 325 | for (Configuration config : configuration.getExtendsFrom()) { 326 | configurations.addAll(getConfigurations(config)); 327 | } 328 | return configurations; 329 | } 330 | 331 | @SuppressWarnings("unchecked") 332 | private void getShadowJarRelocations(List relocations) { 333 | Task shadowJar = getProject().getTasksByName("shadowJar", true) 334 | .stream().findAny().orElse(null); 335 | if (shadowJar == null) { 336 | return; 337 | } 338 | 339 | List patterns = new ArrayList<>(); 340 | List replacements = new ArrayList<>(); 341 | List> includes = new ArrayList<>(); 342 | List> excludes = new ArrayList<>(); 343 | for (Map.Entry entry : shadowJar.getInputs().getProperties().entrySet()) { 344 | String[] keyParts = entry.getKey().split("\\."); 345 | if (keyParts.length != 3) { 346 | continue; 347 | } 348 | if (!keyParts[0].equals("relocators")) { 349 | continue; 350 | } 351 | 352 | int index; 353 | try { 354 | index = Integer.parseInt(keyParts[1].substring(1)); 355 | } catch (NumberFormatException ignored) { 356 | continue; 357 | } 358 | 359 | Object value = entry.getValue(); 360 | String key = keyParts[2]; 361 | switch (key) { 362 | case "pattern": 363 | set(patterns, index, (String) value); 364 | break; 365 | case "shadedPattern": 366 | set(replacements, index, (String) value); 367 | break; 368 | case "includes": 369 | set(includes, index, (Collection) value); 370 | break; 371 | case "excludes": 372 | set(excludes, index, (Collection) value); 373 | break; 374 | } 375 | } 376 | 377 | for (int index = 0; index < patterns.size(); index++) { 378 | relocations.add(new Relocation( 379 | patterns.get(index), 380 | replacements.get(index), 381 | new ArrayList<>(includes.get(index)), 382 | new ArrayList<>(excludes.get(index)) 383 | )); 384 | } 385 | } 386 | 387 | private void set(List list, int index, T value) { 388 | int size = list.size(); 389 | if (size < index) { 390 | for (int i = size; i < index; i++) { 391 | list.add(i, null); 392 | } 393 | } else if (size > index) { 394 | list.set(index, value); 395 | return; 396 | } 397 | list.add(index, value); 398 | } 399 | 400 | private List processDependency(ResolvedDependency dependency, String hashingAlgorithm) throws NoSuchAlgorithmException, IOException { 401 | String hash = null; 402 | String snapshotVersion = null; 403 | String classifier = null; 404 | for (ResolvedArtifact moduleArtifact : dependency.getModuleArtifacts()) { 405 | if (!moduleArtifact.getType().equals("jar")) { 406 | continue; 407 | } 408 | 409 | String currentClassifier = moduleArtifact.getClassifier(); 410 | if (currentClassifier != null) { 411 | classifier = currentClassifier; 412 | } 413 | 414 | File file = moduleArtifact.getFile(); 415 | MessageDigest digest = MessageDigest.getInstance(hashingAlgorithm); 416 | hash = HashUtil.getFileHash(file.toPath(), digest); 417 | 418 | ComponentArtifactIdentifier componentArtifactIdentifier = moduleArtifact.getId(); 419 | ComponentIdentifier componentIdentifier = componentArtifactIdentifier.getComponentIdentifier(); 420 | if (componentIdentifier instanceof MavenUniqueSnapshotComponentIdentifier) { 421 | snapshotVersion = ((MavenUniqueSnapshotComponentIdentifier) componentIdentifier).getTimestampedVersion(); 422 | } 423 | break; 424 | } 425 | 426 | List dependencies = new ArrayList<>(); 427 | if (hash != null) { 428 | String group = dependency.getModuleGroup(); 429 | String module = dependency.getModuleName(); 430 | String version = dependency.getModuleVersion(); 431 | String finalVersion = snapshotVersion != null ? version + ":" + snapshotVersion : version; 432 | if (finalVersion.endsWith("-SNAPSHOT")) { 433 | Logger logger = getLogger(); 434 | logger.warn(""); 435 | logger.warn(group + ":" + module + " resolved to a non-versioned snapshot version: " + version); 436 | logger.warn("This is usually caused by the dependency being resolved from mavenLocal()"); 437 | logger.warn("and the local repository containing the dependency with the version '" + version + "' (without a timestamp)"); 438 | } 439 | dependencies.add(new Dependency(group, module, finalVersion, classifier, hash)); 440 | } 441 | 442 | for (ResolvedDependency child : dependency.getChildren()) { 443 | dependencies.addAll( 444 | processDependency(child, hashingAlgorithm) 445 | ); 446 | } 447 | return dependencies; 448 | } 449 | } 450 | -------------------------------------------------------------------------------- /runtime/src/main/java/dev/vankka/dependencydownload/DependencyManager.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2021-2025 Vankka 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package dev.vankka.dependencydownload; 26 | 27 | import dev.vankka.dependencydownload.classpath.ClasspathAppender; 28 | import dev.vankka.dependencydownload.common.util.HashUtil; 29 | import dev.vankka.dependencydownload.dependency.Dependency; 30 | import dev.vankka.dependencydownload.logger.Logger; 31 | import dev.vankka.dependencydownload.path.CleanupPathProvider; 32 | import dev.vankka.dependencydownload.path.DependencyPathProvider; 33 | import dev.vankka.dependencydownload.relocation.Relocation; 34 | import dev.vankka.dependencydownload.repository.Repository; 35 | import dev.vankka.dependencydownload.resource.DependencyDownloadResource; 36 | import org.jetbrains.annotations.NotNull; 37 | import org.jetbrains.annotations.Nullable; 38 | 39 | import java.io.*; 40 | import java.lang.reflect.Constructor; 41 | import java.lang.reflect.InvocationTargetException; 42 | import java.lang.reflect.Method; 43 | import java.net.MalformedURLException; 44 | import java.net.URLConnection; 45 | import java.nio.file.Files; 46 | import java.nio.file.Path; 47 | import java.security.MessageDigest; 48 | import java.security.NoSuchAlgorithmException; 49 | import java.util.*; 50 | import java.util.concurrent.CompletableFuture; 51 | import java.util.concurrent.CopyOnWriteArrayList; 52 | import java.util.concurrent.Executor; 53 | import java.util.concurrent.atomic.AtomicInteger; 54 | import java.util.function.BiConsumer; 55 | import java.util.function.BiFunction; 56 | import java.util.function.Consumer; 57 | 58 | /** 59 | * The main class responsible for downloading, optionally relocating and loading in dependencies. 60 | */ 61 | @SuppressWarnings("unused") // API 62 | public class DependencyManager { 63 | 64 | private final DependencyPathProvider dependencyPathProvider; 65 | private final Logger logger; 66 | 67 | private final List dependencies = new CopyOnWriteArrayList<>(); 68 | private final List relocations = new CopyOnWriteArrayList<>(); 69 | 70 | /** 71 | * 0 initial, 1 download, 2 relocate, 3 load. 72 | */ 73 | private final AtomicInteger step = new AtomicInteger(0); 74 | 75 | /** 76 | * Creates a {@link DependencyManager}. 77 | * @param dependencyPathProvider the {@link DependencyPathProvider} used for deciding where to store downloaded and relocated dependencies 78 | */ 79 | public DependencyManager(@NotNull DependencyPathProvider dependencyPathProvider) { 80 | this(dependencyPathProvider, Logger.NOOP); 81 | } 82 | 83 | /** 84 | * Creates a {@link DependencyManager}. 85 | * @param dependencyPathProvider the {@link DependencyPathProvider} used for deciding where to store downloaded and relocated dependencies 86 | * @param logger the logger to use 87 | */ 88 | public DependencyManager(@NotNull DependencyPathProvider dependencyPathProvider, @NotNull Logger logger) { 89 | this.dependencyPathProvider = dependencyPathProvider; 90 | this.logger = logger; 91 | } 92 | 93 | /** 94 | * Gets the dependency path provider for this {@link DependencyManager}. 95 | * @return the {@link DependencyPathProvider} being used 96 | */ 97 | @NotNull 98 | public DependencyPathProvider getDependencyPathProvider() { 99 | return dependencyPathProvider; 100 | } 101 | 102 | /** 103 | * Gets the logger being used by this {@link DependencyManager}. 104 | * @return the {@link Logger} being used 105 | */ 106 | @NotNull 107 | public Logger getLogger() { 108 | return logger; 109 | } 110 | 111 | /** 112 | * Adds dependencies to this {@link DependencyManager}. 113 | * @param dependencies the dependencies to add 114 | * @throws IllegalStateException if this method is executed after downloading 115 | * @see #addDependencies(Collection) 116 | */ 117 | public DependencyManager addDependencies(@NotNull Dependency... dependencies) { 118 | return addDependencies(Arrays.asList(dependencies)); 119 | } 120 | 121 | /** 122 | * Adds dependencies to this {@link DependencyManager}. 123 | * @param dependencies the dependencies to add 124 | * @throws IllegalStateException if this method is executed after downloading 125 | * @see #addDependencies(Dependency...) 126 | */ 127 | public DependencyManager addDependencies(@NotNull Collection dependencies) { 128 | if (step.get() > 0) { 129 | throw new IllegalStateException("Cannot add dependencies after downloading"); 130 | } 131 | this.dependencies.addAll(dependencies); 132 | return this; 133 | } 134 | 135 | /** 136 | * Gets the dependencies in this {@link DependencyManager}. 137 | * @return an unmodifiable list of dependencies 138 | */ 139 | @NotNull 140 | public List getDependencies() { 141 | return Collections.unmodifiableList(dependencies); 142 | } 143 | 144 | /** 145 | * Adds relocations to this {@link DependencyManager}. 146 | * @param relocations the relocations to add 147 | * @throws IllegalStateException if this method is executed after relocating 148 | * @see #addRelocations(Collection) 149 | */ 150 | public DependencyManager addRelocations(@NotNull Relocation... relocations) { 151 | return addRelocations(Arrays.asList(relocations)); 152 | } 153 | 154 | /** 155 | * Adds relocations to this {@link DependencyManager}. 156 | * @param relocations the relocations to add 157 | * @throws IllegalStateException if this method is executed after relocating 158 | * @see #addRelocations(Relocation...) 159 | */ 160 | public DependencyManager addRelocations(@NotNull Collection relocations) { 161 | if (step.get() > 2) { 162 | throw new IllegalStateException("Cannot add relocations after relocating"); 163 | } 164 | this.relocations.addAll(relocations); 165 | return this; 166 | } 167 | 168 | /** 169 | * Gets the relocations in this {@link DependencyManager}. 170 | * @return an unmodifiable set of relocations 171 | */ 172 | @NotNull 173 | public List getRelocations() { 174 | return Collections.unmodifiableList(relocations); 175 | } 176 | 177 | /** 178 | * Are this {@link DependencyManager}s dependencies already loaded. 179 | * @return {@code true} if {@link #load(Executor, ClasspathAppender)} has already been loaded 180 | */ 181 | public boolean isLoaded() { 182 | return step.get() == 3; 183 | } 184 | 185 | /** 186 | * Loads dependencies and relocations from the resource provided. 187 | * 188 | * @param resource the resource 189 | */ 190 | public DependencyManager loadResource(@NotNull DependencyDownloadResource resource) { 191 | dependencies.addAll(resource.getDependencies()); 192 | relocations.addAll(resource.getRelocations()); 193 | return this; 194 | } 195 | 196 | /** 197 | * Download all the dependencies in this {@link DependencyManager}. 198 | * 199 | * @param executor the executor that will run the download for every dependency, or {@code null} to run sequentially on the current thread 200 | * @param repositories an ordered list of repositories that will be tried one-by-one in order 201 | * @return a future that will complete exceptionally if a single dependency fails to download from all repositories, 202 | * otherwise completes when all dependencies are downloaded 203 | * @throws IllegalStateException if dependencies have already been queued for download once 204 | */ 205 | public CompletableFuture downloadAll(@Nullable Executor executor, @NotNull List repositories) { 206 | return CompletableFuture.allOf(download(executor, repositories)); 207 | } 208 | 209 | /** 210 | * Download all the dependencies in this {@link DependencyManager}. 211 | * If one of the downloads fails, the rest will not be tried and will not get {@link CompletableFuture}s. 212 | * 213 | * @param executor the executor that will run the download for every dependency, or {@code null} to run sequentially on the current thread 214 | * @param repositories an ordered list of repositories that will be tried one-by-one, in order 215 | * @return an array containing a {@link CompletableFuture} for at least one dependency but up to one for each dependency 216 | * @throws IllegalStateException if dependencies have already been queued for download once 217 | */ 218 | public CompletableFuture[] download(@Nullable Executor executor, @NotNull List repositories) { 219 | if (repositories.isEmpty()) { 220 | throw new IllegalArgumentException("No repositories provided"); 221 | } 222 | // If step is 0 (initial) change to 1 223 | if (!step.compareAndSet(0, 1)) { 224 | throw new IllegalStateException("Download has already been executed"); 225 | } 226 | 227 | logger.downloadStart(); 228 | try { 229 | return forEachDependency( 230 | executor, 231 | dependency -> downloadDependency(dependency, repositories, () -> logger.downloadDependency(dependency)), 232 | (dependency, cause) -> new RuntimeException("Failed to download dependency " + dependency.getGAV(), cause), 233 | logger::downloadSuccess, 234 | logger::downloadFailed 235 | ); 236 | } finally { 237 | logger.downloadEnd(); 238 | } 239 | } 240 | 241 | /** 242 | * Relocates all the dependencies with the relocations in this {@link DependencyManager}. This step is not required. 243 | * Uses the {@link ClassLoader} that loaded this class to acquire {@code jar-relocator}. 244 | * 245 | * @param executor the executor that will run the relocation for every dependency, or {@code null} to run sequentially on the current thread 246 | * @return a future that will complete exceptionally if any of the dependencies fail to 247 | * relocate otherwise completes when all dependencies are relocated 248 | * @throws IllegalStateException if dependencies have already been queued for relocation once 249 | * @see #relocateAll(Executor, ClassLoader) 250 | * @see #relocate(Executor) 251 | * @see #relocate(Executor, ClassLoader) 252 | */ 253 | public CompletableFuture relocateAll(@Nullable Executor executor) { 254 | return CompletableFuture.allOf(relocate(executor, getClass().getClassLoader())); 255 | } 256 | 257 | /** 258 | * Relocates all the dependencies with the relocations in this {@link DependencyManager}. This step is not required. 259 | * 260 | * @param executor the executor that will run the relocation for every dependency, or {@code null} to run sequentially on the current thread 261 | * @param jarRelocatorLoader the {@link ClassLoader} to use to load {@code jar-relocator}, 262 | * if this is set to {@code null} the current class loader will be used 263 | * @return a future that will complete exceptionally if any of the dependencies fail to 264 | * relocate otherwise completes when all dependencies are relocated 265 | * @throws IllegalStateException if dependencies have already been queued for relocation once 266 | * @see #relocateAll(Executor) 267 | * @see #relocate(Executor) 268 | * @see #relocate(Executor, ClassLoader) 269 | */ 270 | public CompletableFuture relocateAll(@Nullable Executor executor, @Nullable ClassLoader jarRelocatorLoader) { 271 | return CompletableFuture.allOf(relocate(executor, jarRelocatorLoader)); 272 | } 273 | 274 | /** 275 | * Relocates all the dependencies with the relocations in this {@link DependencyManager}. This step is not required. 276 | * Uses the {@link ClassLoader} that loaded this class to acquire {@code jar-relocator}. 277 | * If one of the relocation fails, the rest will not be tried and will not get {@link CompletableFuture}s. 278 | * 279 | * @param executor the executor that will run the relocation for every dependency, or {@code null} to run sequentially on the current thread 280 | * @return an array containing a {@link CompletableFuture} for at least one dependency but up to one for each dependency 281 | * @throws IllegalStateException if dependencies have already been queued for relocation once 282 | * @see #relocateAll(Executor, ClassLoader) 283 | * @see #relocateAll(Executor) 284 | * @see #relocate(Executor, ClassLoader) 285 | */ 286 | public CompletableFuture[] relocate(@Nullable Executor executor) { 287 | return relocate(executor, null); 288 | } 289 | 290 | /** 291 | * Relocates all the dependencies with the relocations in this {@link DependencyManager}. This step is not required. 292 | * If one of the relocation fails, the rest will not be tried and will not get {@link CompletableFuture}s. 293 | * 294 | * @param executor the executor that will run the relocation for every dependency, or {@code null} to run sequentially on the current thread 295 | * @param jarRelocatorLoader the {@link ClassLoader} to use to load {@code jar-relocator} 296 | * @return an array containing a {@link CompletableFuture} for at least one dependency but up to one for each dependency 297 | * @throws IllegalStateException if dependencies have already been queued for relocation once 298 | * @see #relocateAll(Executor, ClassLoader) 299 | * @see #relocateAll(Executor) 300 | * @see #relocate(Executor) 301 | */ 302 | public CompletableFuture[] relocate(@Nullable Executor executor, @Nullable ClassLoader jarRelocatorLoader) { 303 | // If step is 1 (load) change to 2, otherwise don't alter 304 | int currentStep = step.getAndUpdate(current -> current == 1 ? 2 : current); 305 | if (currentStep == 0) { 306 | throw new IllegalStateException("Download hasn't been executed"); 307 | } else if (currentStep == 2) { 308 | throw new IllegalStateException("Already relocated"); 309 | } else if (currentStep == 3) { 310 | throw new IllegalStateException("Cannot relocate after loading"); 311 | } 312 | 313 | JarRelocatorHelper helper = new JarRelocatorHelper( 314 | jarRelocatorLoader != null ? jarRelocatorLoader : getClass().getClassLoader(), 315 | relocations 316 | ); 317 | 318 | try { 319 | logger.relocateStart(); 320 | return forEachDependency( 321 | executor, 322 | dependency -> { 323 | logger.relocateDependency(dependency); 324 | return relocateDependency(dependency, helper); 325 | }, 326 | (dependency, cause) -> new RuntimeException("Failed to relocate dependency " + dependency.getGAV(), cause), 327 | logger::relocateSuccess, 328 | logger::relocateFailed 329 | ); 330 | } finally { 331 | logger.relocateEnd(); 332 | } 333 | } 334 | 335 | /** 336 | * Loads all the (potentially relocated) dependencies with provided {@link ClasspathAppender}. 337 | * 338 | * @param executor the executor that will run the provided classpath appender for every dependency, or {@code null} to run sequentially on the current thread 339 | * @param classpathAppender the classpath appender, that will handle loading the file 340 | * @return a future that will complete exceptionally if any of the dependencies fail to 341 | * be appended by the provided {@link ClasspathAppender} otherwise completes when all dependencies are relocated 342 | * @throws IllegalStateException if dependencies have already been queued for load once 343 | */ 344 | public CompletableFuture loadAll(@Nullable Executor executor, @NotNull ClasspathAppender classpathAppender) { 345 | return CompletableFuture.allOf(load(executor, classpathAppender)); 346 | } 347 | 348 | /** 349 | * Loads all the (potentially relocated) dependencies with provided {@link ClasspathAppender}. 350 | * If one of the loads fails, the rest will not be tried and will not get {@link CompletableFuture}s. 351 | * 352 | * @param executor the executor that will run the provided classpath appender for every dependency, or {@code null} to run sequentially on the current thread 353 | * @param classpathAppender the classpath appender 354 | * @return an array containing a {@link CompletableFuture} for at least one dependency but up to one for each dependency 355 | * @throws IllegalStateException if dependencies have already been queued for load once 356 | */ 357 | public CompletableFuture[] load(@Nullable Executor executor, @NotNull ClasspathAppender classpathAppender) { 358 | // If step is 1 (download), 2 (relocate) change to 3 (load), otherwise keep current 359 | int currentStep = step.getAndUpdate(current -> current == 0 || current == 3 ? current : 3); 360 | if (currentStep == 0) { 361 | throw new IllegalStateException("Download hasn't been executed"); 362 | } else if (currentStep == 3) { 363 | throw new IllegalStateException("Already loaded"); 364 | } 365 | 366 | try { 367 | logger.loadStart(); 368 | 369 | return forEachDependency( 370 | executor, 371 | dependency -> { 372 | logger.loadDependency(dependency); 373 | return loadDependency(dependency, classpathAppender, currentStep == 2); 374 | }, 375 | (dependency, cause) -> new RuntimeException("Failed to load dependency " + dependency.getGAV(), cause), 376 | logger::loadSuccess, 377 | logger::loadFailed 378 | ); 379 | } finally { 380 | logger.loadEnd(); 381 | } 382 | } 383 | 384 | /** 385 | * Gets the {@link Path} where the given {@link Dependency} will be stored once downloaded or relocated. 386 | * 387 | * @param dependency the dependency. 388 | * @param relocated if the path should be for the relocated or unrelocated (downloaded) file of the Dependency 389 | * @return the path for the dependency 390 | */ 391 | @NotNull 392 | public Path getPathForDependency(@NotNull Dependency dependency, boolean relocated) { 393 | return getDependencyPathProvider().getDependencyPath(dependency, relocated); 394 | } 395 | 396 | /** 397 | * Gets {@link Path}s to all {@link Dependency Dependencies} in this {@link DependencyManager}. Including ones that do not exist. 398 | * @param relocated the paths for all relocated files, otherwise all unrelocated (downloaded) files 399 | * @return paths to all dependencies, original or relocated 400 | * @see #getPathForDependency(Dependency, boolean) 401 | */ 402 | @NotNull 403 | public Set getPaths(boolean relocated) { 404 | Set paths = new HashSet<>(); 405 | for (Dependency dependency : dependencies) { 406 | paths.add(getPathForDependency(dependency, relocated)); 407 | } 408 | return paths; 409 | } 410 | 411 | /** 412 | * Gets {@link Path}s to all {@link Dependency Dependencies} in this {@link DependencyManager}, 413 | * optionally also including the relocated paths if {@code includeRelocated} is set to {@code true}. 414 | * Including ones that do not exist. 415 | * 416 | * @param includeRelocated if relocated paths should also be included 417 | * @return paths to all dependencies (and optionally relocated dependencies) 418 | * @see #getPathForDependency(Dependency, boolean) 419 | */ 420 | @NotNull 421 | public Set getAllPaths(boolean includeRelocated) { 422 | Set paths = new HashSet<>(getPaths(false)); 423 | if (includeRelocated) { 424 | paths.addAll(getPaths(true)); 425 | } 426 | return paths; 427 | } 428 | 429 | /** 430 | * Removes files that are not known dependencies of this {@link DependencyManager} from {@link CleanupPathProvider#getPathsForAllStoredDependencies()} implementation. 431 | * 432 | * This only accounts for dependencies that are included in this {@link DependencyManager} instance! 433 | * 434 | * 435 | * @throws IOException if listing files in the cache directory or deleting files in it fails 436 | * @throws IllegalStateException if this DependencyManager's dependencyPathProvider isn't an instance of {@link CleanupPathProvider} 437 | * @see #getAllPaths(boolean) 438 | * @see CleanupPathProvider 439 | */ 440 | public void cleanupCacheDirectory() throws IOException, IllegalStateException { 441 | if (!(dependencyPathProvider instanceof CleanupPathProvider)) { 442 | throw new IllegalStateException("Cache directory cleanup is only available when dependencyPathProvider is a instance of CleanupPathProvider"); 443 | } 444 | 445 | Collection existingPaths = ((CleanupPathProvider) dependencyPathProvider).getPathsForAllStoredDependencies(); 446 | Set currentPaths = getAllPaths(true); 447 | for (Path existingPath : existingPaths) { 448 | if (Files.isDirectory(existingPath)) { 449 | continue; 450 | } 451 | 452 | if (!currentPaths.contains(existingPath)) { 453 | Files.delete(existingPath); 454 | } 455 | } 456 | } 457 | 458 | @SuppressWarnings("unchecked") 459 | private CompletableFuture[] forEachDependency( 460 | Executor executor, 461 | Step runnable, 462 | BiFunction dependencyException, 463 | Consumer successLog, 464 | BiConsumer failLog 465 | ) { 466 | int size = dependencies.size(); 467 | CompletableFuture[] futures = new CompletableFuture[size]; 468 | 469 | for (int index = 0; index < size; index++) { 470 | Dependency dependency = dependencies.get(index); 471 | 472 | CompletableFuture future = new CompletableFuture<>(); 473 | Runnable run = () -> { 474 | try { 475 | boolean stepPerformed = runnable.run(dependency); 476 | if (stepPerformed) { 477 | successLog.accept(dependency); 478 | } 479 | 480 | future.complete(null); 481 | } catch (Throwable t) { 482 | future.completeExceptionally(dependencyException.apply(dependency, t)); 483 | failLog.accept(dependency, t); 484 | } 485 | }; 486 | 487 | if (executor != null) { 488 | executor.execute(run); 489 | } else { 490 | run.run(); 491 | } 492 | 493 | futures[index] = future; 494 | if (future.isCompletedExceptionally()) { 495 | // don't need to bother with the rest if one fails 496 | break; 497 | } 498 | } 499 | 500 | return futures; 501 | } 502 | 503 | private boolean downloadDependency(Dependency dependency, List repositories, Runnable beginDownloadCallback) 504 | throws IOException, NoSuchAlgorithmException { 505 | Path dependencyPath = getPathForDependency(dependency, false); 506 | 507 | if (!Files.exists(dependencyPath.getParent())) { 508 | Files.createDirectories(dependencyPath.getParent()); 509 | } 510 | 511 | MessageDigest digest = MessageDigest.getInstance(dependency.getHashingAlgorithm()); 512 | if (Files.exists(dependencyPath)) { 513 | String fileHash = HashUtil.getFileHash(dependencyPath, digest); 514 | if (fileHash.equals(dependency.getHash())) { 515 | // This dependency is already downloaded & the hash matches -> skip download 516 | return false; 517 | } else { 518 | // Hash does not match, delete file 519 | Files.delete(dependencyPath); 520 | } 521 | } 522 | beginDownloadCallback.run(); 523 | Files.createFile(dependencyPath); 524 | 525 | RuntimeException failure = new RuntimeException("All provided repositories failed to download dependency"); 526 | boolean anyFailures = false; 527 | for (Repository repository : repositories) { 528 | try { 529 | digest.reset(); 530 | downloadFromRepository(dependency, repository, dependencyPath, digest); 531 | 532 | String hash = HashUtil.getHash(digest); 533 | String dependencyHash = dependency.getHash(); 534 | if (!hash.equals(dependencyHash)) { 535 | throw new SecurityException("Failed to verify file hash: " + hash + " should've been: " + dependencyHash); 536 | } 537 | 538 | // Success 539 | return true; 540 | } catch (Exception e) { 541 | Files.deleteIfExists(dependencyPath); 542 | failure.addSuppressed(e); 543 | anyFailures = true; 544 | } 545 | } 546 | if (!anyFailures) { 547 | throw new IllegalStateException("Nothing failed yet nothing passed"); 548 | } 549 | throw failure; 550 | } 551 | 552 | private void downloadFromRepository( 553 | Dependency dependency, 554 | Repository repository, 555 | Path dependencyPath, 556 | MessageDigest digest 557 | ) throws IOException { 558 | URLConnection connection = repository.openConnection(dependency); 559 | 560 | byte[] buffer = new byte[repository.getBufferSize()]; 561 | try (BufferedInputStream inputStream = new BufferedInputStream(connection.getInputStream())) { 562 | try (BufferedOutputStream outputStream = new BufferedOutputStream(Files.newOutputStream(dependencyPath))) { 563 | int total; 564 | while ((total = inputStream.read(buffer)) != -1) { 565 | outputStream.write(buffer, 0, total); 566 | digest.update(buffer, 0, total); 567 | } 568 | } 569 | } 570 | } 571 | 572 | private boolean relocateDependency(Dependency dependency, JarRelocatorHelper helper) { 573 | Path dependencyFile = getPathForDependency(dependency, false); 574 | Path relocatedFile = getPathForDependency(dependency, true); 575 | 576 | try { 577 | helper.run(dependencyFile, relocatedFile); 578 | } catch (InvocationTargetException e) { 579 | throw new RuntimeException("Failed to run relocation", e.getCause()); 580 | } catch (ReflectiveOperationException e) { 581 | throw new RuntimeException("Failed to initialize relocator", e); 582 | } 583 | return true; 584 | } 585 | 586 | private boolean loadDependency( 587 | Dependency dependency, 588 | ClasspathAppender classpathAppender, 589 | boolean relocated 590 | ) throws MalformedURLException { 591 | Path fileToLoad = relocated 592 | ? getPathForDependency(dependency, true) 593 | : getPathForDependency(dependency, false); 594 | 595 | classpathAppender.appendFileToClasspath(fileToLoad); 596 | return true; 597 | } 598 | 599 | @FunctionalInterface 600 | private interface Step { 601 | 602 | /** 603 | * @return {@code true} if the step was performed, {@code false} if skipped 604 | */ 605 | boolean run(T t) throws Throwable; 606 | } 607 | 608 | private static class JarRelocatorHelper { 609 | 610 | private final Constructor relocatorConstructor; 611 | private final Method relocatorRunMethod; 612 | 613 | private final List mappedRelocations; 614 | 615 | public JarRelocatorHelper(ClassLoader classLoader, List relocations) { 616 | try { 617 | Class relocatorClass = classLoader.loadClass("me.lucko.jarrelocator.JarRelocator"); 618 | this.relocatorConstructor = relocatorClass.getConstructor(File.class, File.class, Collection.class); 619 | this.relocatorRunMethod = relocatorClass.getMethod("run"); 620 | 621 | Class relocationClass = classLoader.loadClass("me.lucko.jarrelocator.Relocation"); 622 | Constructor relocationConstructor = relocationClass.getConstructor(String.class, String.class, Collection.class, Collection.class); 623 | 624 | this.mappedRelocations = new ArrayList<>(); 625 | for (Relocation relocation : relocations) { 626 | Object mapped = relocationConstructor.newInstance( 627 | relocation.getPattern(), 628 | relocation.getShadedPattern(), 629 | relocation.getIncludes(), 630 | relocation.getExcludes() 631 | ); 632 | mappedRelocations.add(mapped); 633 | } 634 | } catch (ReflectiveOperationException e) { 635 | throw new RuntimeException("Failed to load jar-relocator from the provided ClassLoader", e); 636 | } 637 | } 638 | 639 | public void run(Path from, Path to) throws ReflectiveOperationException { 640 | Object relocator = relocatorConstructor.newInstance(from.toFile(), to.toFile(), mappedRelocations); 641 | relocatorRunMethod.invoke(relocator); 642 | } 643 | } 644 | } 645 | --------------------------------------------------------------------------------