excludes = new LinkedList<>();
130 |
131 | /**
132 | * Sets the search pattern.
133 | *
134 | * @param pattern pattern to search
135 | * @return this builder
136 | */
137 | public Builder pattern(String pattern) {
138 | this.pattern = requireNonNull(pattern, "pattern");
139 | return this;
140 | }
141 |
142 | /**
143 | * Sets the replacement pattern.
144 | *
145 | * @param relocatedPattern pattern to replace with
146 | * @return this builder
147 | */
148 | public Builder relocatedPattern(String relocatedPattern) {
149 | this.relocatedPattern = requireNonNull(relocatedPattern, "relocatedPattern");
150 | return this;
151 | }
152 |
153 | /**
154 | * Adds a class or resource to be included.
155 | *
156 | * @param include class or resource to include
157 | * @return this builder
158 | */
159 | public Builder include(String include) {
160 | includes.add(requireNonNull(include, "include"));
161 | return this;
162 | }
163 |
164 | /**
165 | * Adds a class or resource to be excluded.
166 | *
167 | * @param exclude class or resource to exclude
168 | * @return this builder
169 | */
170 | public Builder exclude(String exclude) {
171 | excludes.add(requireNonNull(exclude, "exclude"));
172 | return this;
173 | }
174 |
175 | /**
176 | * Creates a new relocation using this builder's configuration.
177 | *
178 | * @return new relocation
179 | */
180 | public Relocation build() {
181 | return new Relocation(pattern, relocatedPattern, includes, excludes);
182 | }
183 | }
184 | }
185 |
--------------------------------------------------------------------------------
/core/src/main/java/net/byteflux/libby/logging/Logger.java:
--------------------------------------------------------------------------------
1 | package net.byteflux.libby.logging;
2 |
3 | import net.byteflux.libby.logging.adapters.LogAdapter;
4 |
5 | import static java.util.Objects.requireNonNull;
6 |
7 | /**
8 | * A logging wrapper that logs to a log adapter and can be configured to filter
9 | * log messages by severity.
10 | */
11 | public class Logger {
12 | /**
13 | * Log adapter for the current platform
14 | */
15 | private final LogAdapter adapter;
16 |
17 | /**
18 | * Log level controlling which messages are logged
19 | */
20 | private LogLevel level = LogLevel.INFO;
21 |
22 | /**
23 | * Creates a new logger with the provided adapter.
24 | *
25 | * @param adapter the adapter to wrap
26 | */
27 | public Logger(LogAdapter adapter) {
28 | this.adapter = requireNonNull(adapter, "adapter");
29 | }
30 |
31 | /**
32 | * Gets the current log level.
33 | *
34 | * @return current log level
35 | */
36 | public LogLevel getLevel() {
37 | return level;
38 | }
39 |
40 | /**
41 | * Sets a new log level.
42 | *
43 | * @param level new log level
44 | */
45 | public void setLevel(LogLevel level) {
46 | this.level = requireNonNull(level, "level");
47 | }
48 |
49 | /**
50 | * Gets whether messages matching the provided level can be logged under
51 | * the current log level setting.
52 | *
53 | * Returns true if provided log level is equal to or more severe than the
54 | * logger's configured log level.
55 | *
56 | * @param level the level to check
57 | * @return true if message can be logged, or false
58 | */
59 | private boolean canLog(LogLevel level) {
60 | return requireNonNull(level, "level").compareTo(this.level) >= 0;
61 | }
62 |
63 | /**
64 | * Logs a message with the provided level.
65 | *
66 | * If the provided log level is less severe than the logger's
67 | * configured log level, this message won't be logged.
68 | *
69 | * @param level message severity level
70 | * @param message the message to log
71 | * @see #debug(String)
72 | * @see #info(String)
73 | * @see #warn(String)
74 | * @see #error(String)
75 | */
76 | public void log(LogLevel level, String message) {
77 | if (canLog(level)) {
78 | adapter.log(level, message);
79 | }
80 | }
81 |
82 | /**
83 | * Logs a message and stack trace with the provided level.
84 | *
85 | * If the provided log level is less severe than the logger's
86 | * configured log level, this message won't be logged.
87 | *
88 | * @param level message severity level
89 | * @param message the message to log
90 | * @param throwable the throwable to print
91 | * @see #debug(String, Throwable)
92 | * @see #info(String, Throwable)
93 | * @see #warn(String, Throwable)
94 | * @see #error(String, Throwable)
95 | */
96 | public void log(LogLevel level, String message, Throwable throwable) {
97 | if (canLog(level)) {
98 | adapter.log(level, message, throwable);
99 | }
100 | }
101 |
102 | /**
103 | * Logs a debug message.
104 | *
105 | * If the logger's configured log level is more severe than
106 | * {@link LogLevel#DEBUG}, this message won't be logged.
107 | *
108 | * @param message the message to log
109 | */
110 | public void debug(String message) {
111 | log(LogLevel.DEBUG, message);
112 | }
113 |
114 | /**
115 | * Logs a debug message with a stack trace.
116 | *
117 | * If the logger's configured log level is more severe than
118 | * {@link LogLevel#DEBUG}, this message won't be logged.
119 | *
120 | * @param message the message to log
121 | * @param throwable the throwable to print
122 | */
123 | public void debug(String message, Throwable throwable) {
124 | log(LogLevel.DEBUG, message, throwable);
125 | }
126 |
127 | /**
128 | * Logs an informational message.
129 | *
130 | * If the logger's configured log level is more severe than
131 | * {@link LogLevel#INFO}, this message won't be logged.
132 | *
133 | * @param message the message to log
134 | */
135 | public void info(String message) {
136 | log(LogLevel.INFO, message);
137 | }
138 |
139 | /**
140 | * Logs an informational message with a stack trace.
141 | *
142 | * If the logger's configured log level is more severe than
143 | * {@link LogLevel#INFO}, this message won't be logged.
144 | *
145 | * @param message the message to log
146 | * @param throwable the throwable to print
147 | */
148 | public void info(String message, Throwable throwable) {
149 | log(LogLevel.INFO, message, throwable);
150 | }
151 |
152 | /**
153 | * Logs a warning message.
154 | *
155 | * If the logger's configured log level is more severe than
156 | * {@link LogLevel#WARN}, this message won't be logged.
157 | *
158 | * @param message the message to log
159 | */
160 | public void warn(String message) {
161 | log(LogLevel.WARN, message);
162 | }
163 |
164 | /**
165 | * Logs a warning message with a stack trace.
166 | *
167 | * If the logger's configured log level is more severe than
168 | * {@link LogLevel#WARN}, this message won't be logged.
169 | *
170 | * @param message the message to log
171 | * @param throwable the throwable to print
172 | */
173 | public void warn(String message, Throwable throwable) {
174 | log(LogLevel.WARN, message, throwable);
175 | }
176 |
177 | /**
178 | * Logs an error message.
179 | *
180 | * @param message the message to log
181 | */
182 | public void error(String message) {
183 | log(LogLevel.ERROR, message);
184 | }
185 |
186 | /**
187 | * Logs an error message with a stack trace.
188 | *
189 | * @param message message to log
190 | * @param throwable the throwable to print
191 | */
192 | public void error(String message, Throwable throwable) {
193 | log(LogLevel.ERROR, message, throwable);
194 | }
195 | }
196 |
--------------------------------------------------------------------------------
/core/src/main/java/net/byteflux/libby/Library.java:
--------------------------------------------------------------------------------
1 | package net.byteflux.libby;
2 |
3 | import net.byteflux.libby.relocation.Relocation;
4 |
5 | import java.util.Base64;
6 | import java.util.Collection;
7 | import java.util.Collections;
8 | import java.util.LinkedList;
9 |
10 | import static java.util.Objects.requireNonNull;
11 |
12 | /**
13 | * An immutable representation of a Maven artifact that can be downloaded,
14 | * relocated and then loaded into a plugin's classpath at runtime.
15 | *
16 | * @see #builder()
17 | */
18 | public class Library {
19 | /**
20 | * Direct download URLs for this library
21 | */
22 | private final Collection urls;
23 |
24 | /**
25 | * Maven group ID
26 | */
27 | private final String groupId;
28 |
29 | /**
30 | * Maven artifact ID
31 | */
32 | private final String artifactId;
33 |
34 | /**
35 | * Artifact version
36 | */
37 | private final String version;
38 |
39 | /**
40 | * Artifact classifier
41 | */
42 | private final String classifier;
43 |
44 | /**
45 | * Binary SHA-256 checksum for this library's jar file
46 | */
47 | private final byte[] checksum;
48 |
49 | /**
50 | * Jar relocations to apply
51 | */
52 | private final Collection relocations;
53 |
54 | /**
55 | * Relative Maven path to this library's artifact
56 | */
57 | private final String path;
58 |
59 | /**
60 | * Relative path to this library's relocated jar
61 | */
62 | private final String relocatedPath;
63 |
64 | /**
65 | * Creates a new library.
66 | *
67 | * @param urls direct download URLs
68 | * @param groupId Maven group ID
69 | * @param artifactId Maven artifact ID
70 | * @param version artifact version
71 | * @param classifier artifact classifier or null
72 | * @param checksum binary SHA-256 checksum or null
73 | * @param relocations jar relocations or null
74 | */
75 | private Library(Collection urls,
76 | String groupId,
77 | String artifactId,
78 | String version,
79 | String classifier,
80 | byte[] checksum,
81 | Collection relocations) {
82 |
83 | this.urls = urls != null ? Collections.unmodifiableList(new LinkedList<>(urls)) : Collections.emptyList();
84 | this.groupId = requireNonNull(groupId, "groupId").replace("{}", ".");
85 | this.artifactId = requireNonNull(artifactId, "artifactId");
86 | this.version = requireNonNull(version, "version");
87 | this.classifier = classifier;
88 | this.checksum = checksum;
89 | this.relocations = relocations != null ? Collections.unmodifiableList(new LinkedList<>(relocations)) : Collections.emptyList();
90 |
91 | String path = this.groupId.replace('.', '/') + '/' + artifactId + '/' + version + '/' + artifactId + '-' + version;
92 | if (hasClassifier()) {
93 | path += '-' + classifier;
94 | }
95 |
96 | this.path = path + ".jar";
97 | relocatedPath = hasRelocations() ? path + "-relocated.jar" : null;
98 | }
99 |
100 | /**
101 | * Gets the direct download URLs for this library.
102 | *
103 | * @return direct download URLs
104 | */
105 | public Collection getUrls() {
106 | return urls;
107 | }
108 |
109 | /**
110 | * Gets the Maven group ID for this library.
111 | *
112 | * @return Maven group ID
113 | */
114 | public String getGroupId() {
115 | return groupId;
116 | }
117 |
118 | /**
119 | * Gets the Maven artifact ID for this library.
120 | *
121 | * @return Maven artifact ID
122 | */
123 | public String getArtifactId() {
124 | return artifactId;
125 | }
126 |
127 | /**
128 | * Gets the artifact version for this library.
129 | *
130 | * @return artifact version
131 | */
132 | public String getVersion() {
133 | return version;
134 | }
135 |
136 | /**
137 | * Gets the artifact classifier for this library.
138 | *
139 | * @return artifact classifier or null
140 | */
141 | public String getClassifier() {
142 | return classifier;
143 | }
144 |
145 | /**
146 | * Gets whether this library has an artifact classifier.
147 | *
148 | * @return true if library has classifier, false otherwise
149 | */
150 | public boolean hasClassifier() {
151 | return classifier != null;
152 | }
153 |
154 | /**
155 | * Gets the binary SHA-256 checksum of this library's jar file.
156 | *
157 | * @return checksum or null
158 | */
159 | public byte[] getChecksum() {
160 | return checksum;
161 | }
162 |
163 | /**
164 | * Gets whether this library has a checksum.
165 | *
166 | * @return true if library has checksum, false otherwise
167 | */
168 | public boolean hasChecksum() {
169 | return checksum != null;
170 | }
171 |
172 | /**
173 | * Gets the jar relocations to apply to this library.
174 | *
175 | * @return jar relocations to apply
176 | */
177 | public Collection getRelocations() {
178 | return relocations;
179 | }
180 |
181 | /**
182 | * Gets whether this library has any jar relocations.
183 | *
184 | * @return true if library has relocations, false otherwise
185 | */
186 | public boolean hasRelocations() {
187 | return !relocations.isEmpty();
188 | }
189 |
190 | /**
191 | * Gets the relative Maven path to this library's artifact.
192 | *
193 | * @return Maven path for this library
194 | */
195 | public String getPath() {
196 | return path;
197 | }
198 |
199 | /**
200 | * Gets the relative path to this library's relocated jar.
201 | *
202 | * @return path to relocated artifact or null if has no relocations
203 | */
204 | public String getRelocatedPath() {
205 | return relocatedPath;
206 | }
207 |
208 | /**
209 | * Gets a concise, human-readable string representation of this library.
210 | *
211 | * @return string representation
212 | */
213 | @Override
214 | public String toString() {
215 | String name = groupId + ':' + artifactId + ':' + version;
216 | if (hasClassifier()) {
217 | name += ':' + classifier;
218 | }
219 |
220 | return name;
221 | }
222 |
223 | /**
224 | * Creates a new library builder.
225 | *
226 | * @return new library builder
227 | */
228 | public static Builder builder() {
229 | return new Builder();
230 | }
231 |
232 | /**
233 | * Due to the constructor complexity of an immutable {@link Library},
234 | * instead this fluent builder is used to configure and then construct
235 | * a new library.
236 | */
237 | public static class Builder {
238 | /**
239 | * Direct download URLs for this library
240 | */
241 | private final Collection urls = new LinkedList<>();
242 |
243 | /**
244 | * Maven group ID
245 | */
246 | private String groupId;
247 |
248 | /**
249 | * Maven artifact ID
250 | */
251 | private String artifactId;
252 |
253 | /**
254 | * Artifact version
255 | */
256 | private String version;
257 |
258 | /**
259 | * Artifact classifier
260 | */
261 | private String classifier;
262 |
263 | /**
264 | * Binary SHA-256 checksum for this library's jar file
265 | */
266 | private byte[] checksum;
267 |
268 | /**
269 | * Jar relocations to apply
270 | */
271 | private final Collection relocations = new LinkedList<>();
272 |
273 | /**
274 | * Adds a direct download URL for this library.
275 | *
276 | * @param url direct download URL
277 | * @return this builder
278 | */
279 | public Builder url(String url) {
280 | urls.add(requireNonNull(url, "url"));
281 | return this;
282 | }
283 |
284 | /**
285 | * Sets the Maven group ID for this library.
286 | *
287 | * @param groupId Maven group ID
288 | * @return this builder
289 | */
290 | public Builder groupId(String groupId) {
291 | this.groupId = requireNonNull(groupId, "groupId");
292 | return this;
293 | }
294 |
295 | /**
296 | * Sets the Maven artifact ID for this library.
297 | *
298 | * @param artifactId Maven artifact ID
299 | * @return this builder
300 | */
301 | public Builder artifactId(String artifactId) {
302 | this.artifactId = requireNonNull(artifactId, "artifactId");
303 | return this;
304 | }
305 |
306 | /**
307 | * Sets the artifact version for this library.
308 | *
309 | * @param version artifact version
310 | * @return this builder
311 | */
312 | public Builder version(String version) {
313 | this.version = requireNonNull(version, "version");
314 | return this;
315 | }
316 |
317 | /**
318 | * Sets the artifact classifier for this library.
319 | *
320 | * @param classifier artifact classifier
321 | * @return this builder
322 | */
323 | public Builder classifier(String classifier) {
324 | this.classifier = requireNonNull(classifier, "classifier");
325 | return this;
326 | }
327 |
328 | /**
329 | * Sets the binary SHA-256 checksum for this library.
330 | *
331 | * @param checksum binary SHA-256 checksum
332 | * @return this builder
333 | */
334 | public Builder checksum(byte[] checksum) {
335 | this.checksum = requireNonNull(checksum, "checksum");
336 | return this;
337 | }
338 |
339 | /**
340 | * Sets the Base64-encoded SHA-256 checksum for this library.
341 | *
342 | * @param checksum Base64-encoded SHA-256 checksum
343 | * @return this builder
344 | */
345 | public Builder checksum(String checksum) {
346 | return checksum(Base64.getDecoder().decode(requireNonNull(checksum, "checksum")));
347 | }
348 |
349 | /**
350 | * Adds a jar relocation to apply to this library.
351 | *
352 | * @param relocation jar relocation to apply
353 | * @return this builder
354 | */
355 | public Builder relocate(Relocation relocation) {
356 | relocations.add(requireNonNull(relocation, "relocation"));
357 | return this;
358 | }
359 |
360 | /**
361 | * Adds a jar relocation to apply to this library.
362 | *
363 | * @param pattern search pattern
364 | * @param relocatedPattern replacement pattern
365 | * @return this builder
366 | */
367 | public Builder relocate(String pattern, String relocatedPattern) {
368 | return relocate(new Relocation(pattern, relocatedPattern));
369 | }
370 |
371 | /**
372 | * Creates a new library using this builder's configuration.
373 | *
374 | * @return new library
375 | */
376 | public Library build() {
377 | return new Library(urls, groupId, artifactId, version, classifier, checksum, relocations);
378 | }
379 | }
380 | }
381 |
--------------------------------------------------------------------------------
/core/src/main/java/net/byteflux/libby/LibraryManager.java:
--------------------------------------------------------------------------------
1 | package net.byteflux.libby;
2 |
3 | import net.byteflux.libby.logging.LogLevel;
4 | import net.byteflux.libby.logging.Logger;
5 | import net.byteflux.libby.logging.adapters.LogAdapter;
6 | import net.byteflux.libby.relocation.Relocation;
7 | import net.byteflux.libby.relocation.RelocationHelper;
8 |
9 | import java.io.ByteArrayOutputStream;
10 | import java.io.FileNotFoundException;
11 | import java.io.IOException;
12 | import java.io.InputStream;
13 | import java.io.UncheckedIOException;
14 | import java.net.MalformedURLException;
15 | import java.net.SocketTimeoutException;
16 | import java.net.URL;
17 | import java.net.URLConnection;
18 | import java.net.UnknownHostException;
19 | import java.nio.file.Files;
20 | import java.nio.file.Path;
21 | import java.nio.file.Paths;
22 | import java.security.MessageDigest;
23 | import java.security.NoSuchAlgorithmException;
24 | import java.util.Arrays;
25 | import java.util.Base64;
26 | import java.util.Collection;
27 | import java.util.Collections;
28 | import java.util.LinkedList;
29 | import java.util.List;
30 |
31 | import static java.util.Objects.requireNonNull;
32 |
33 | /**
34 | * A runtime dependency manager for plugins.
35 | *
36 | * The library manager can resolve a dependency jar through the configured
37 | * Maven repositories, download it into a local cache, relocate it and then
38 | * load it into the plugin's classpath.
39 | *
40 | * Transitive dependencies for a library aren't downloaded automatically and
41 | * must be explicitly loaded like every other library.
42 | *
43 | * It's recommended that libraries are relocated to prevent any namespace
44 | * conflicts with different versions of the same library bundled with other
45 | * plugins or maybe even bundled with the server itself.
46 | *
47 | * @see Library
48 | */
49 | public abstract class LibraryManager {
50 | /**
51 | * Wrapped plugin logger
52 | */
53 | protected final Logger logger;
54 |
55 | /**
56 | * Directory where downloaded library jars are saved to
57 | */
58 | protected final Path saveDirectory;
59 |
60 | /**
61 | * Maven repositories used to resolve artifacts
62 | */
63 | private final List repositories = new LinkedList<>();
64 |
65 | /**
66 | * Lazily-initialized relocation helper that uses reflection to call into
67 | * Luck's Jar Relocator
68 | */
69 | private RelocationHelper relocator;
70 |
71 | /**
72 | * Creates a new library manager.
73 | *
74 | * @param logAdapter plugin logging adapter
75 | * @param dataDirectory plugin's data directory
76 | */
77 | protected LibraryManager(LogAdapter logAdapter, Path dataDirectory) {
78 | logger = new Logger(requireNonNull(logAdapter, "logAdapter"));
79 | saveDirectory = requireNonNull(dataDirectory, "dataDirectory").toAbsolutePath().resolve("lib");
80 | }
81 |
82 | /**
83 | * Adds a file to the plugin's classpath.
84 | *
85 | * @param file the file to add
86 | */
87 | protected abstract void addToClasspath(Path file);
88 |
89 | /**
90 | * Gets the logging level for this library manager.
91 | *
92 | * @return log level
93 | */
94 | public LogLevel getLogLevel() {
95 | return logger.getLevel();
96 | }
97 |
98 | /**
99 | * Sets the logging level for this library manager.
100 | *
101 | * By setting this value, the library manager's logger will not log any
102 | * messages with a level less severe than the configured level. This can be
103 | * useful for silencing the download and relocation logging.
104 | *
105 | * Setting this value to {@link LogLevel#WARN} would silence informational
106 | * logging but still print important things like invalid checksum warnings.
107 | *
108 | * @param level the log level to set
109 | */
110 | public void setLogLevel(LogLevel level) {
111 | logger.setLevel(level);
112 | }
113 |
114 | /**
115 | * Gets the currently added repositories used to resolve artifacts.
116 | *
117 | * For each library this list is traversed to download artifacts after the
118 | * direct download URLs have been attempted.
119 | *
120 | * @return current repositories
121 | */
122 | public Collection getRepositories() {
123 | List urls;
124 | synchronized (repositories) {
125 | urls = new LinkedList<>(repositories);
126 | }
127 |
128 | return Collections.unmodifiableList(urls);
129 | }
130 |
131 | /**
132 | * Adds a repository URL to this library manager.
133 | *
134 | * Artifacts will be resolved using this repository when attempts to locate
135 | * the artifact through previously added repositories are all unsuccessful.
136 | *
137 | * @param url repository URL to add
138 | */
139 | public void addRepository(String url) {
140 | String repo = requireNonNull(url, "url").endsWith("/") ? url : url + '/';
141 | synchronized (repositories) {
142 | repositories.add(repo);
143 | }
144 | }
145 |
146 | /**
147 | * Adds the current user's local Maven repository.
148 | */
149 | public void addMavenLocal() {
150 | addRepository(Paths.get(System.getProperty("user.home")).resolve(".m2/repository").toUri().toString());
151 | }
152 |
153 | /**
154 | * Adds the Maven Central repository.
155 | */
156 | public void addMavenCentral() {
157 | addRepository("https://repo1.maven.org/maven2/");
158 | }
159 |
160 | /**
161 | * Adds the Sonatype OSS repository.
162 | */
163 | public void addSonatype() {
164 | addRepository("https://oss.sonatype.org/content/groups/public/");
165 | }
166 |
167 | /**
168 | * Adds the Bintray JCenter repository.
169 | */
170 | public void addJCenter() {
171 | addRepository("https://jcenter.bintray.com/");
172 | }
173 |
174 | /**
175 | * Adds the JitPack repository.
176 | */
177 | public void addJitPack() {
178 | addRepository("https://jitpack.io/");
179 | }
180 |
181 | /**
182 | * Gets all of the possible download URLs for this library. Entries are
183 | * ordered by direct download URLs first and then repository download URLs.
184 | *
185 | * @param library the library to resolve
186 | * @return download URLs
187 | */
188 | public Collection resolveLibrary(Library library) {
189 | List urls = new LinkedList<>(requireNonNull(library, "library").getUrls());
190 | for (String repository : getRepositories()) {
191 | urls.add(repository + library.getPath());
192 | }
193 |
194 | return Collections.unmodifiableList(urls);
195 | }
196 |
197 | /**
198 | * Downloads a library jar and returns the contents as a byte array.
199 | *
200 | * @param url the URL to the library jar
201 | * @return downloaded jar as byte array or null if nothing was downloaded
202 | */
203 | private byte[] downloadLibrary(String url) {
204 | try {
205 | URLConnection connection = new URL(requireNonNull(url, "url")).openConnection();
206 |
207 | connection.setConnectTimeout(5000);
208 | connection.setReadTimeout(5000);
209 | connection.setRequestProperty("User-Agent", LibbyProperties.HTTP_USER_AGENT);
210 |
211 | try (InputStream in = connection.getInputStream()) {
212 | int len;
213 | byte[] buf = new byte[8192];
214 | ByteArrayOutputStream out = new ByteArrayOutputStream();
215 |
216 | try {
217 | while ((len = in.read(buf)) != -1) {
218 | out.write(buf, 0, len);
219 | }
220 | } catch (SocketTimeoutException e) {
221 | logger.warn("Download timed out: " + connection.getURL());
222 | return null;
223 | }
224 |
225 | logger.info("Downloaded library " + connection.getURL());
226 | return out.toByteArray();
227 | }
228 | } catch (MalformedURLException e) {
229 | throw new IllegalArgumentException(e);
230 | } catch (IOException e) {
231 | if (e instanceof FileNotFoundException) {
232 | logger.debug("File not found: " + url);
233 | } else if (e instanceof SocketTimeoutException) {
234 | logger.debug("Connect timed out: " + url);
235 | } else if (e instanceof UnknownHostException) {
236 | logger.debug("Unknown host: " + url);
237 | } else {
238 | logger.debug("Unexpected IOException", e);
239 | }
240 |
241 | return null;
242 | }
243 | }
244 |
245 | /**
246 | * Downloads a library jar to the save directory if it doesn't already
247 | * exist and returns the local file path.
248 | *
249 | * If the library has a checksum, it will be compared against the
250 | * downloaded jar's checksum to verify the integrity of the download. If
251 | * the checksums don't match, a warning is generated and the next download
252 | * URL is attempted.
253 | *
254 | * Checksum comparison is ignored if the library doesn't have a checksum
255 | * or if the library jar already exists in the save directory.
256 | *
257 | * Most of the time it is advised to use {@link #loadLibrary(Library)}
258 | * instead of this method because this one is only concerned with
259 | * downloading the jar and returning the local path. It's usually more
260 | * desirable to download the jar and add it to the plugin's classpath in
261 | * one operation.
262 | *
263 | * @param library the library to download
264 | * @return local file path to library
265 | * @see #loadLibrary(Library)
266 | */
267 | public Path downloadLibrary(Library library) {
268 | Path file = saveDirectory.resolve(requireNonNull(library, "library").getPath());
269 | if (Files.exists(file)) {
270 | return file;
271 | }
272 |
273 | Collection urls = resolveLibrary(library);
274 | if (urls.isEmpty()) {
275 | throw new RuntimeException("Library '" + library + "' couldn't be resolved, add a repository");
276 | }
277 |
278 | MessageDigest md = null;
279 | if (library.hasChecksum()) {
280 | try {
281 | md = MessageDigest.getInstance("SHA-256");
282 | } catch (NoSuchAlgorithmException e) {
283 | throw new RuntimeException(e);
284 | }
285 | }
286 |
287 | Path out = file.resolveSibling(file.getFileName() + ".tmp");
288 | out.toFile().deleteOnExit();
289 |
290 | try {
291 | Files.createDirectories(file.getParent());
292 |
293 | for (String url : urls) {
294 | byte[] bytes = downloadLibrary(url);
295 | if (bytes == null) {
296 | continue;
297 | }
298 |
299 | if (md != null) {
300 | byte[] checksum = md.digest(bytes);
301 | if (!Arrays.equals(checksum, library.getChecksum())) {
302 | logger.warn("*** INVALID CHECKSUM ***");
303 | logger.warn(" Library : " + library);
304 | logger.warn(" URL : " + url);
305 | logger.warn(" Expected : " + Base64.getEncoder().encodeToString(library.getChecksum()));
306 | logger.warn(" Actual : " + Base64.getEncoder().encodeToString(checksum));
307 | continue;
308 | }
309 | }
310 |
311 | Files.write(out, bytes);
312 | Files.move(out, file);
313 |
314 | return file;
315 | }
316 | } catch (IOException e) {
317 | throw new UncheckedIOException(e);
318 | } finally {
319 | try {
320 | Files.deleteIfExists(out);
321 | } catch (IOException ignored) {
322 | }
323 | }
324 |
325 | throw new RuntimeException("Failed to download library '" + library + "'");
326 | }
327 |
328 | /**
329 | * Processes the input jar and generates an output jar with the provided
330 | * relocation rules applied, then returns the path to the relocated jar.
331 | *
332 | * @param in input jar
333 | * @param out output jar
334 | * @param relocations relocations to apply
335 | * @return the relocated file
336 | * @see RelocationHelper#relocate(Path, Path, Collection)
337 | */
338 | private Path relocate(Path in, String out, Collection relocations) {
339 | requireNonNull(in, "in");
340 | requireNonNull(out, "out");
341 | requireNonNull(relocations, "relocations");
342 |
343 | Path file = saveDirectory.resolve(out);
344 | if (Files.exists(file)) {
345 | return file;
346 | }
347 |
348 | Path tmpOut = file.resolveSibling(file.getFileName() + ".tmp");
349 | tmpOut.toFile().deleteOnExit();
350 |
351 | synchronized (this) {
352 | if (relocator == null) {
353 | relocator = new RelocationHelper(this);
354 | }
355 | }
356 |
357 | try {
358 | relocator.relocate(in, tmpOut, relocations);
359 | Files.move(tmpOut, file);
360 |
361 | logger.info("Relocations applied to " + saveDirectory.getParent().relativize(in));
362 |
363 | return file;
364 | } catch (IOException e) {
365 | throw new UncheckedIOException(e);
366 | } finally {
367 | try {
368 | Files.deleteIfExists(tmpOut);
369 | } catch (IOException ignored) {
370 | }
371 | }
372 | }
373 |
374 | /**
375 | * Loads a library jar into the plugin's classpath. If the library jar
376 | * doesn't exist locally, it will be downloaded.
377 | *
378 | * If the provided library has any relocations, they will be applied to
379 | * create a relocated jar and the relocated jar will be loaded instead.
380 | *
381 | * @param library the library to load
382 | * @see #downloadLibrary(Library)
383 | */
384 | public void loadLibrary(Library library) {
385 | Path file = downloadLibrary(requireNonNull(library, "library"));
386 | if (library.hasRelocations()) {
387 | file = relocate(file, library.getRelocatedPath(), library.getRelocations());
388 | }
389 |
390 | addToClasspath(file);
391 | }
392 | }
393 |
--------------------------------------------------------------------------------