/* * 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.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; import com.google.common.base.MoreObjects; import org.lanternpowered.server.game.Lantern; import org.spongepowered.api.plugin.PluginContainer; import org.spongepowered.plugin.meta.PluginDependency; import org.spongepowered.plugin.meta.PluginMetadata; import org.spongepowered.plugin.meta.version.DefaultArtifactVersion; import org.spongepowered.plugin.meta.version.InvalidVersionSpecificationException; import org.spongepowered.plugin.meta.version.VersionRange; import java.math.BigInteger; import java.nio.file.Path; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.Map; import java.util.Optional; import java.util.Set; import javax.annotation.Nullable; public final class PluginCandidate { private final String id; private final String pluginClass; private final Optional<Path> source; private PluginMetadata metadata; private boolean invalid; @Nullable private Set<PluginCandidate> dependencies; @Nullable private Set<PluginCandidate> requirements; private final Set<String> dependenciesWithUnknownVersion = new HashSet<>(); @Nullable private Map<String, String> versions; @Nullable private Map<String, String> missingRequirements; PluginCandidate(String pluginClass, @Nullable Path source, PluginMetadata metadata) { this.pluginClass = checkNotNull(pluginClass, "pluginClass"); this.source = Optional.ofNullable(source); this.metadata = checkNotNull(metadata, "metadata"); this.id = metadata.getId(); } public String getId() { return this.id; } public String getPluginClass() { return this.pluginClass; } public Optional<Path> getSource() { return this.source; } public String getDisplaySource() { if (this.source.isPresent()) { return this.source.get().toString(); } return "unknown"; } public PluginMetadata getMetadata() { return this.metadata; } void setMetadata(PluginMetadata metadata) { this.metadata = checkNotNull(metadata, "metadata"); } public boolean isInvalid() { return this.invalid; } public boolean isLoadable() { return !this.invalid && getMissingRequirements().isEmpty(); } public boolean dependenciesCollected() { return this.dependencies != null; } private void ensureState() { checkState(dependenciesCollected(), "Dependencies not collected yet"); } public Set<PluginCandidate> getDependencies() { ensureState(); return this.dependencies; } public Set<PluginCandidate> getRequirements() { ensureState(); return this.requirements; } public Map<String, String> getMissingRequirements() { ensureState(); return this.missingRequirements; } public String getVersion(String id) { ensureState(); return this.versions.get(id); } public boolean updateRequirements() { ensureState(); if (this.requirements.isEmpty()) { return false; } Iterator<PluginCandidate> itr = this.requirements.iterator(); while (itr.hasNext()) { final PluginCandidate candidate = itr.next(); if (!candidate.isLoadable()) { itr.remove(); this.missingRequirements.put(candidate.getId(), this.versions.get(candidate.getId())); } } return this.invalid || !this.missingRequirements.isEmpty(); } public boolean collectDependencies(Map<String, PluginContainer> loadedPlugins, Map<String, PluginCandidate> candidates) { checkState(this.dependencies == null, "Dependencies already collected"); if (loadedPlugins.containsKey(this.id)) { this.invalid = true; } this.dependencies = new HashSet<>(); this.requirements = new HashSet<>(); this.versions = new HashMap<>(); this.missingRequirements = new HashMap<>(); for (PluginDependency dependency : this.metadata.collectRequiredDependencies()) { final String id = dependency.getId(); if (this.id.equals(id)) { Lantern.getLogger().warn("Plugin '{}' from {} requires itself to be loaded. " + "This is redundant and can be removed from the dependencies.", this.id, getDisplaySource()); continue; } final String version = dependency.getVersion(); final PluginContainer loaded = loadedPlugins.get(id); if (loaded != null) { if (!verifyVersionRange(id, version, loaded.getVersion().orElse(null))) { this.missingRequirements.put(id, version); } continue; } final PluginCandidate candidate = candidates.get(id); if (candidate != null && verifyVersionRange(id, version, candidate.getMetadata().getVersion())) { this.requirements.add(candidate); continue; } this.missingRequirements.put(id, version); } final Map<PluginDependency.LoadOrder, Set<PluginDependency>> dependencies = this.metadata.groupDependenciesByLoadOrder(); collectOptionalDependencies(dependencies.get(PluginDependency.LoadOrder.BEFORE), loadedPlugins, candidates); final Set<PluginDependency> loadAfter = dependencies.get(PluginDependency.LoadOrder.AFTER); if (loadAfter != null && !loadAfter.isEmpty()) { this.invalid = true; Lantern.getLogger().error("Invalid dependency with load order BEFORE on plugin '{}' from {}. " + "This is currently not supported for Sponge plugins! Requested dependencies: {}", this.id, getDisplaySource(), loadAfter); } return isLoadable(); } private void collectOptionalDependencies(@Nullable Iterable<PluginDependency> dependencies, Map<String, PluginContainer> loadedPlugins, Map<String, PluginCandidate> candidates) { if (dependencies == null) { return; } for (PluginDependency dependency : dependencies) { final String id = dependency.getId(); if (this.id.equals(id)) { Lantern.getLogger().error("Plugin '{}' from {} cannot have a dependency on itself. This is redundant and should be " + "removed.", this.id, getDisplaySource()); this.invalid = true; continue; } final String version = dependency.getVersion(); final PluginContainer loaded = loadedPlugins.get(id); if (loaded != null) { if (!verifyVersionRange(id, version, loaded.getVersion().orElse(null))) { this.missingRequirements.put(id, version); } continue; } final PluginCandidate candidate = candidates.get(id); if (candidate != null) { if (verifyVersionRange(id, version, candidate.getMetadata().getVersion())) { this.dependencies.add(candidate); } else { this.missingRequirements.put(id, version); } } } } private boolean verifyVersionRange(String id, @Nullable String expectedRange, @Nullable String version) { if (expectedRange == null) { return true; } // Don't check version again if it already failed if (expectedRange.equals(this.missingRequirements.get(id))) { return false; } // Don't check version again if it was already checked if (expectedRange.equals(this.versions.get(id))) { return true; } if (version != null) { try { final VersionRange range = VersionRange.createFromVersionSpec(expectedRange); final DefaultArtifactVersion installedVersion = new DefaultArtifactVersion(version); if (range.containsVersion(installedVersion)) { final String currentRange = this.versions.get(id); if (currentRange != null) { // This should almost never happen because it means the plugin is // depending on two different versions of another plugin // We need to merge the ranges final VersionRange otherRange; try { otherRange = VersionRange.createFromVersionSpec(currentRange); } catch (InvalidVersionSpecificationException e) { throw new AssertionError(e); // Should never happen because we already parsed it once } expectedRange = otherRange.restrict(range).toString(); } this.versions.put(id, expectedRange); if (range.getRecommendedVersion() instanceof DefaultArtifactVersion) { final BigInteger majorExpected = ((DefaultArtifactVersion) range.getRecommendedVersion()).getVersion().getFirstInteger(); if (majorExpected != null) { final BigInteger majorInstalled = installedVersion.getVersion().getFirstInteger(); // Show a warning if the major version does not match, // or if the installed version is lower than the recommended version if (majorInstalled != null && (!majorExpected.equals(majorInstalled) || installedVersion.compareTo(range.getRecommendedVersion()) < 0)) { Lantern.getLogger().warn("Plugin {} from {} was designed for {} {}. It may not work properly.", this.id, this.source, id, range.getRecommendedVersion()); } } } return true; } } catch (InvalidVersionSpecificationException e) { Lantern.getLogger().error("Failed to parse version range {} for dependency {} of plugin {} from {}: {}", version, id, this.id, getDisplaySource(), e.getMessage()); this.invalid = true; } } else { if (this.dependenciesWithUnknownVersion.add(id)) { Lantern.getLogger().warn("Cannot check version of dependency {} for plugin {} from {}: Version of dependency unknown", id, this.id, this.source); } return true; } return false; } @Override public boolean equals(@Nullable Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } final PluginCandidate candidate = (PluginCandidate) o; return this.id.equals(candidate.id); } @Override public int hashCode() { return this.id.hashCode(); } @Override public String toString() { final MoreObjects.ToStringHelper helper = MoreObjects.toStringHelper(this) .omitNullValues() .add("id", this.id) .add("class", this.pluginClass) .add("source", this.source.orElse(null)); if (this.invalid) { helper.addValue("INVALID"); } else if (this.missingRequirements != null && !this.missingRequirements.isEmpty()) { helper.addValue("FAILED"); } return helper.toString(); } }