/*
* This file is part of LanternServer, licensed under the MIT License (MIT).
*
* Copyright (c) LanternPowered <https://www.lanternpowered.org>
* Copyright (c) SpongePowered <https://www.spongepowered.org>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the Software), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.lanternpowered.server.plugin;
import static com.google.common.base.MoreObjects.firstNonNull;
import static com.google.common.base.Preconditions.checkNotNull;
import com.google.common.collect.ImmutableList;
import org.lanternpowered.server.game.Lantern;
import org.lanternpowered.server.game.LanternGame;
import org.lanternpowered.server.util.ClassLoaderUtil;
import org.spongepowered.api.plugin.PluginContainer;
import org.spongepowered.api.plugin.PluginManager;
import org.spongepowered.plugin.meta.PluginMetadata;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URLClassLoader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
public final class LanternPluginManager implements PluginManager {
private final Map<String, PluginContainer> plugins = new HashMap<>();
private final Map<Object, PluginContainer> pluginInstances = new IdentityHashMap<>();
private final LanternGame game;
private final Path pluginsFolder;
public LanternPluginManager(LanternGame game, Path pluginsFolder) {
this.pluginsFolder = checkNotNull(pluginsFolder, "pluginsFolder");
this.game = checkNotNull(game, "game");
}
public void registerPlugin(PluginContainer plugin) {
checkNotNull(plugin, "plugin");
this.plugins.put(plugin.getId(), plugin);
}
private void registerPluginInstance(PluginContainer plugin) {
checkNotNull(plugin, "plugin");
this.pluginInstances.put(plugin.getInstance().orElseThrow(
() -> new IllegalStateException("Plugin instance missing.")), plugin);
}
public void registerPluginInstances() {
for (Map.Entry<String, PluginContainer> entry : this.plugins.entrySet()) {
entry.getValue().getInstance().ifPresent(instance -> registerPluginInstance(entry.getValue()));
}
}
public void loadPlugins(boolean scanClasspath) throws IOException {
this.game.getLogger().info("Searching for plugins...");
PluginScanner pluginScanner = new PluginScanner();
if (scanClasspath) {
Lantern.getLogger().info("Scanning classpath for plugins...");
final ClassLoader loader = LanternPluginManager.class.getClassLoader();
if (loader instanceof URLClassLoader) {
pluginScanner.scanClassPath((URLClassLoader) loader);
} else {
this.game.getLogger().error("Cannot search for plugins on classpath: Unsupported class loader: {}",
loader.getClass().getName());
}
}
if (Files.exists(this.pluginsFolder)) {
pluginScanner.scanDirectory(this.pluginsFolder);
} else {
// Create plugin folder
Files.createDirectories(this.pluginsFolder);
}
final Map<String, PluginCandidate> plugins = pluginScanner.getPlugins();
this.game.getLogger().info("{} plugin(s) found", plugins.size());
try {
PluginHelper.sort(checkRequirements(plugins)).forEach(this::loadPlugin);
} catch (Throwable e) {
throw new RuntimeException("An error occurred while loading the plugins", e);
}
}
private Set<PluginCandidate> checkRequirements(Map<String, PluginCandidate> candidates) {
Set<PluginCandidate> successfulCandidates = new HashSet<>(candidates.size());
List<PluginCandidate> failedCandidates = new ArrayList<>();
for (PluginCandidate candidate : candidates.values()) {
if (candidate.collectDependencies(this.plugins, candidates)) {
successfulCandidates.add(candidate);
} else {
failedCandidates.add(candidate);
}
}
if (failedCandidates.isEmpty()) {
return successfulCandidates; // Nothing to do, all requirements satisfied
}
PluginCandidate candidate;
boolean updated;
while (true) {
updated = false;
Iterator<PluginCandidate> itr = successfulCandidates.iterator();
while (itr.hasNext()) {
candidate = itr.next();
if (candidate.updateRequirements()) {
updated = true;
itr.remove();
failedCandidates.add(candidate);
}
}
if (updated) {
// Update failed candidates as well
failedCandidates.forEach(PluginCandidate::updateRequirements);
} else {
break;
}
}
for (PluginCandidate failed : failedCandidates) {
if (failed.isInvalid()) {
this.game.getLogger().error("Plugin '{}' from {} cannot be loaded because it is invalid",
failed.getId(), failed.getDisplaySource());
} else {
this.game.getLogger().error("Cannot load plugin '{}' from {} because it is missing the required dependencies {}",
failed.getId(), failed.getDisplaySource(), PluginHelper.formatRequirements(failed.getMissingRequirements()));
}
}
return successfulCandidates;
}
private void loadPlugin(PluginCandidate candidate) {
final String id = candidate.getId();
if (candidate.getSource().isPresent()) {
try {
ClassLoaderUtil.addURL((URLClassLoader) LanternPluginManager.class.getClassLoader(),
candidate.getSource().get().toUri().toURL());
} catch (MalformedURLException e) {
throw new RuntimeException("Failed to add plugin '" + id + "' from " + candidate.getDisplaySource() + " to classpath", e);
}
}
final PluginMetadata metadata = candidate.getMetadata();
final String name = firstNonNull(metadata.getName(), id);
final String version = firstNonNull(metadata.getVersion(), "unknown");
try {
final Class<?> pluginClass = Class.forName(candidate.getPluginClass());
final PluginContainer container = new LanternPluginContainer(id, pluginClass, metadata.getName(), metadata.getVersion(),
metadata.getDescription(), metadata.getUrl(), metadata.getAuthors(), candidate.getSource());
registerPlugin(container);
registerPluginInstance(container);
this.game.getEventManager().registerListeners(container, container.getInstance().get());
this.game.getLogger().info("Loaded plugin: {} {} (from {})", name, version, candidate.getDisplaySource());
} catch (Throwable e) {
this.game.getLogger().error("Failed to load plugin: {} {} (from {})", name, version, candidate.getDisplaySource(), e);
}
}
@Override
public Optional<PluginContainer> fromInstance(Object instance) {
checkNotNull(instance, "instance");
if (instance instanceof PluginContainer) {
return Optional.of((PluginContainer) instance);
}
return Optional.ofNullable(this.pluginInstances.get(instance));
}
@Override
public Optional<PluginContainer> getPlugin(String id) {
return Optional.ofNullable(this.plugins.get(checkNotNull(id, "identifier")));
}
@Override
public Collection<PluginContainer> getPlugins() {
return ImmutableList.copyOf(this.plugins.values());
}
@Override
public boolean isLoaded(String id) {
return this.plugins.containsKey(checkNotNull(id, "identifier"));
}
}