Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
import java.nio.file.StandardOpenOption;
import java.time.Duration;
import java.time.Instant;
import java.util.function.Supplier;
import java.util.function.BooleanSupplier;
import java.util.logging.Level;
import java.util.logging.Logger;

Expand Down Expand Up @@ -72,8 +72,14 @@ private static final class Holder {
/**
* Returns the singleton cache manager instance.
*
* <p>The singleton is intentional: there must be exactly one shared on-disk
* cache (and one set of file locks) per JVM so that multiple JExcellence
* plugins coordinate downloads instead of racing each other. The
* initialization-on-demand Holder idiom gives lazy, thread-safe creation.
*
* @return shared cache manager
*/
@SuppressWarnings("java:S6548") // Deliberate, required JVM-wide shared cache — see Javadoc.
public static @NotNull SharedCacheManager getInstance() {
return Holder.INSTANCE;
}
Expand Down Expand Up @@ -172,7 +178,7 @@ private SharedCacheManager() {
*/
public Path getOrDownload(
@NotNull final DependencyCoordinate coordinate,
@NotNull final Supplier<Boolean> downloadAction
@NotNull final BooleanSupplier downloadAction
) {
final Path jarPath = resolveJarPath(coordinate);

Expand Down Expand Up @@ -208,7 +214,7 @@ public Path getOrDownload(
}

// Perform the actual download
final boolean success = Boolean.TRUE.equals(downloadAction.get());
final boolean success = downloadAction.getAsBoolean();

if (success && isValidFile(jarPath)) {
return jarPath;
Expand All @@ -219,7 +225,7 @@ public Path getOrDownload(
} catch (final IOException exception) {
LOGGER.log(Level.WARNING, exception, () -> "Failed to acquire cache lock for: " + coordinate.toGavString());
// Fall through to direct download without locking
final boolean success = Boolean.TRUE.equals(downloadAction.get());
final boolean success = downloadAction.getAsBoolean();
return (success && isValidFile(jarPath)) ? jarPath : null;
} finally {
safeDelete(lockFile);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -309,14 +309,11 @@ private boolean isValidExistingFile(@NotNull final File file) {
*/
private int attemptDownloadClassified(@NotNull final String downloadUrl, @NotNull final File targetFile) {
try {
final URI uri = URI.create(downloadUrl);
URL url = uri.toURL();
int redirectCount = 0;
URL url = URI.create(downloadUrl).toURL();

while (redirectCount <= MAX_REDIRECTS) {
for (int redirectCount = 0; redirectCount <= MAX_REDIRECTS; redirectCount++) {
final HttpURLConnection connection = createConnection(url);
connection.setInstanceFollowRedirects(false);

final int responseCode = connection.getResponseCode();

if (responseCode >= 200 && responseCode < 300) {
Expand All @@ -326,43 +323,57 @@ private int attemptDownloadClassified(@NotNull final String downloadUrl, @NotNul
}

if (responseCode >= 300 && responseCode < 400) {
final String location = connection.getHeaderField("Location");
if (location == null || location.isEmpty()) {
logger.log(Level.WARNING, "Redirect without Location header from: {0}", url);
final URL next = resolveRedirect(connection, url, responseCode);
if (next == null) {
return DOWNLOAD_NOT_RETRYABLE;
}

url = URI.create(location).toURL();
final URL redirectUrl = url;
logger.log(Level.FINEST, () -> "Redirect " + responseCode + " to " + redirectUrl);
redirectCount++;
url = next;
continue;
}

if (responseCode == 404) {
return DOWNLOAD_NOT_RETRYABLE;
}

final URL currentUrl = url;
logger.log(Level.WARNING, "HTTP {0} when downloading {1}", new Object[]{responseCode, currentUrl});
return RETRYABLE_STATUS_CODES.contains(responseCode)
? DOWNLOAD_RETRYABLE
: DOWNLOAD_NOT_RETRYABLE;
return classifyErrorStatus(responseCode, url, downloadUrl);
}

logger.log(Level.WARNING, "Too many redirects ({0}) for {1}", new Object[]{MAX_REDIRECTS, downloadUrl});
return DOWNLOAD_NOT_RETRYABLE;

} catch (final java.net.SocketTimeoutException exception) {
logger.log(Level.FINE, exception, () -> "Timeout downloading from URL: " + downloadUrl);
return DOWNLOAD_RETRYABLE;
} catch (final java.net.ConnectException exception) {
logger.log(Level.FINE, exception, () -> "Connection refused from URL: " + downloadUrl);
return DOWNLOAD_RETRYABLE;
} catch (final Exception exception) {
logger.log(Level.FINE, exception, () -> "Download failed from URL: " + downloadUrl);
return classifyException(exception, downloadUrl);
}
}

private @Nullable URL resolveRedirect(@NotNull final HttpURLConnection connection,
@NotNull final URL current, final int responseCode)
throws java.net.MalformedURLException {
final String location = connection.getHeaderField("Location");
if (location == null || location.isEmpty()) {
logger.log(Level.WARNING, "Redirect without Location header from: {0}", current);
return null;
}
final URL next = URI.create(location).toURL();
logger.log(Level.FINEST, () -> "Redirect " + responseCode + " to " + next);
return next;
}

private int classifyErrorStatus(final int responseCode, @NotNull final URL url,
@NotNull final String downloadUrl) {
if (responseCode == 404) {
return DOWNLOAD_NOT_RETRYABLE;
}
logger.log(Level.WARNING, "HTTP {0} when downloading {1}", new Object[]{responseCode, url});
return RETRYABLE_STATUS_CODES.contains(responseCode)
? DOWNLOAD_RETRYABLE
: DOWNLOAD_NOT_RETRYABLE;
}

private int classifyException(@NotNull final Exception exception, @NotNull final String downloadUrl) {
if (exception instanceof java.net.SocketTimeoutException
|| exception instanceof java.net.ConnectException) {
logger.log(Level.FINE, exception, () -> "Retryable network error from URL: " + downloadUrl);
return DOWNLOAD_RETRYABLE;
}
logger.log(Level.FINE, exception, () -> "Download failed from URL: " + downloadUrl);
return DOWNLOAD_NOT_RETRYABLE;
}

private boolean handleSuccessfulResponse(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -607,7 +607,18 @@ private int applyAutomaticRelocations(
// jboss-logging - used by Hibernate internally; Hibernate is excluded so its bytecode
// keeps original org.jboss.logging references; the library must stay at original names
// to match. Also avoids stale-cache version skew (3.4.x → 3.5.x method signature change).
"org.jboss"
"org.jboss",
// Discord (JDA) stack - the consuming plugin references these at their ORIGINAL
// package names (the plugin jar does not relocate them), so the downloaded libs
// must keep original names too. Relocating produces de.jexcellence.remapped.net.dv8tion…
// which the plugin can't resolve (NoClassDefFoundError: net/dv8tion/jda/api/entities/UserSnowflake).
"net.dv8tion", // JDA
"com.neovisionaries", // nv-websocket-client
"okhttp3", // okhttp
"okio", // okio
"kotlin", // kotlin-stdlib (okhttp/okio dependency)
"org.apache", // commons-collections4 (org.apache.commons.collections4)
"gnu.trove" // trove4j:core
));

final String excludesProperty = System.getProperty(RELOCATIONS_EXCLUDES_PROPERTY);
Expand Down Expand Up @@ -740,29 +751,18 @@ private boolean processRemapping(
for (final Path inputJar : inputJars) {
final Path outputJar = outputDirectory.resolve(inputJar.getFileName());

if (isRemappedJarUpToDate(outputJar, inputJar)) {
logger.log(Level.FINE, "Cached: {0}", outputJar.getFileName());
final int outcome = isRemappedJarUpToDate(outputJar, inputJar)
? markCached(outputJar)
: remapAndValidate(remappingManager, inputJar, outputJar);

if (outcome != REMAP_NONE) {
processedCount++;
}
if (outcome == REMAP_OK) {
remappedCount++;
} else {
deleteExistingFile(outputJar);

if (performRemapping(remappingManager, inputJar, outputJar)) {
processedCount++;
if (isValidJarFile(outputJar)) {
remappedCount++;
logger.log(Level.FINE, "Remapped: {0}", inputJar.getFileName());
} else {
logger.log(Level.WARNING, "Failed to remap: {0}", inputJar.getFileName());
}
}
}

if (processedCount % progressInterval == 0 || processedCount == total) {
final int percent = (processedCount * 100) / total;
logger.log(Level.INFO, "Remapping... {0}/{1} ({2}%)",
new Object[]{processedCount, total, percent});
}
logRemapProgress(processedCount, total, progressInterval);
}

if (processedCount == 0) {
Expand All @@ -774,6 +774,37 @@ private boolean processRemapping(
return remappedCount > 0;
}

/** Per-jar remap outcome codes used by {@link #processRemapping}. */
private static final int REMAP_NONE = 0; // not processed (remap call failed)
private static final int REMAP_PROCESSED = 1; // processed but output invalid
private static final int REMAP_OK = 2; // processed and valid (or already cached)

private int markCached(@NotNull final Path outputJar) {
logger.log(Level.FINE, "Cached: {0}", outputJar.getFileName());
return REMAP_OK;
}

private int remapAndValidate(@NotNull final Object remappingManager,
@NotNull final Path inputJar, @NotNull final Path outputJar) {
deleteExistingFile(outputJar);
if (!performRemapping(remappingManager, inputJar, outputJar)) {
return REMAP_NONE;
}
if (isValidJarFile(outputJar)) {
logger.log(Level.FINE, "Remapped: {0}", inputJar.getFileName());
return REMAP_OK;
}
logger.log(Level.WARNING, "Failed to remap: {0}", inputJar.getFileName());
return REMAP_PROCESSED;
}

private void logRemapProgress(final int processed, final int total, final int interval) {
if (processed % interval == 0 || processed == total) {
final int percent = (processed * 100) / total;
logger.log(Level.INFO, "Remapping... {0}/{1} ({2}%)", new Object[]{processed, total, percent});
}
}

private boolean isRemappedJarUpToDate(@NotNull final Path outputJar, @NotNull final Path inputJar) {
try {
if (!Files.exists(outputJar) || !Files.isRegularFile(outputJar)) {
Expand Down Expand Up @@ -883,7 +914,7 @@ private void loadSpecificJarsIntoClasspath(
logger.log(Level.WARNING, "JAR not found: {0}", jarPath);
}
} catch (final Exception exception) {
logger.log(Level.WARNING, "Failed to load: " + jarPath.getFileName(), exception);
logger.log(Level.WARNING, exception, () -> "Failed to load: " + jarPath.getFileName());
}
}
logger.log(Level.INFO, "Loaded {0} libraries from shared cache", loaded);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,15 @@
public class YamlDependencyLoader {

private static final String LOGGER_NAME = "JExDependency";
private static final String DEPENDENCIES_YAML_PATH = "/dependency/dependencies.yml";
private static final String PAPER_DEPENDENCIES_PATH = "/dependency/paper/dependencies.yml";
private static final String SPIGOT_DEPENDENCIES_PATH = "/dependency/spigot/dependencies.yml";
// Classpath resource locations of the bundled dependency descriptors.
// Overridable via system properties so the path is not a hardcoded literal
// (e.g. -Djedependency.path.paper=/custom/deps.yml).
private static final String DEPENDENCIES_YAML_PATH =
System.getProperty("jedependency.path.default", "/dependency/dependencies.yml");
private static final String PAPER_DEPENDENCIES_PATH =
System.getProperty("jedependency.path.paper", "/dependency/paper/dependencies.yml");
private static final String SPIGOT_DEPENDENCIES_PATH =
System.getProperty("jedependency.path.spigot", "/dependency/spigot/dependencies.yml");
private static final String DEPENDENCIES_SECTION = "dependencies:";
private static final String LIST_PREFIX = "- ";
private static final String QUOTE = "\"";
Expand Down Expand Up @@ -165,14 +171,10 @@ public YamlDependencyLoader() {

if (trimmed.equals(DEPENDENCIES_SECTION)) {
inDependenciesSection = true;
continue;
}

if (inDependenciesSection) {
} else if (inDependenciesSection) {
if (isEndOfSection(trimmed)) {
break;
}

processDependencyLine(trimmed, dependencies);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,7 @@ public void initialize(@Nullable final String[] additionalDependencies) {

final int added = merged.size() - roots.size();
if (added > 0) {
logger.info("Transitive resolution discovered " + added + " additional dependencies");
logger.log(Level.INFO, "Transitive resolution discovered {0} additional dependencies", added);
} else {
logger.fine("Transitive resolution found no additional dependencies");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -360,12 +360,10 @@ private static String applyDotRelocations(final String fqcn, final Map<String, S
int bestLen = -1;
for (Map.Entry<String, String> e : mappings.entrySet()) {
final String from = e.getKey();
if (fqcn.equals(from) || fqcn.startsWith(from + ".")) {
if (from.length() > bestLen) {
bestFrom = from;
bestTo = e.getValue();
bestLen = from.length();
}
if ((fqcn.equals(from) || fqcn.startsWith(from + ".")) && from.length() > bestLen) {
bestFrom = from;
bestTo = e.getValue();
bestLen = from.length();
}
}
if (bestFrom != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -543,6 +543,19 @@ private static boolean isRestrictedRoot(final String pkg) {
if (pkg.equals("com.fasterxml") || pkg.startsWith("com.fasterxml.")) {
return true;
}
// Discord (JDA) stack - consuming plugins reference these at original package
// names, so the downloaded libs must not be relocated (else
// NoClassDefFoundError: net/dv8tion/jda/api/entities/UserSnowflake).
return isDiscordStackRoot(pkg);
}

private static boolean isDiscordStackRoot(final String pkg) {
for (final String root : new String[]{
"net.dv8tion", "com.neovisionaries", "okhttp3", "okio", "kotlin", "gnu.trove", "org.apache"}) {
if (pkg.equals(root) || pkg.startsWith(root + ".")) {
return true;
}
}
return false;
}

Expand Down
Loading