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