/**
* This file is part of Obsidian, licensed under the MIT License (MIT).
*
* Copyright (c) 2013-2014 ObsidianBox <http://obsidianbox.org/>
*
* 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.obsidianbox.obsidian.addon;
import java.io.IOException;
import java.io.InputStreamReader;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.net.URLClassLoader;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import net.minecraftforge.common.MinecraftForge;
import org.apache.logging.log4j.LogManager;
import org.obsidianbox.magma.Game;
import org.obsidianbox.magma.addon.Addon;
import org.obsidianbox.magma.addon.AddonDescription;
import org.obsidianbox.magma.addon.AddonManager;
import org.obsidianbox.magma.addon.InvalidAddonException;
import org.obsidianbox.magma.addon.InvalidDescriptionException;
import org.obsidianbox.magma.util.FileUtils;
import org.obsidianbox.obsidian.resource.CommonFileSystem;
import org.obsidianbox.obsidian.util.map.SerializableHashMap;
public class CommonAddonManager implements AddonManager {
private static final String ADDON_JSON = "addon.info";
protected final Game game;
private final Set<Addon> addons = new HashSet<>();
private final SerializableHashMap addonMD5s = new SerializableHashMap();
private final Addon internal;
public CommonAddonManager(Game game) {
this.game = game;
this.internal = new InternalAddon(game);
addons.add(internal);
}
@Override
public Addon getAddon(String identifier) {
if (isOfficialAddon(identifier)) {
throw new IllegalStateException("Official addons are restricted to access only by the mod.");
}
if (identifier != null && !identifier.isEmpty()) {
for (Addon addon : addons) {
if (addon.getDescription().getIdentifier().equalsIgnoreCase(identifier)) {
return addon;
}
}
}
return null;
}
@Override
public Collection<Addon> getAddons() {
return Collections.unmodifiableCollection(addons);
}
@Override
public Addon loadAddon(Path path) throws InvalidAddonException, InvalidDescriptionException {
final AddonDescription description = create(path);
if (!description.isValidMode(game.getSide())) {
throw new InvalidAddonException("Attempted to load path [" + path + "] but this can only be done on the " + game.getSide().name().toLowerCase() + ".");
}
final Path dataPath = Paths.get(path.getParent().toString(), description.getIdentifier());
try {
final AddonClassLoader loader = new AddonClassLoader((URLClassLoader) MinecraftForge.class.getClassLoader(), path.toUri().toURL());
final Class<?> addonMain = Class.forName(description.getMain(), true, loader);
final Class<? extends Addon> addonClass = addonMain.asSubclass(Addon.class);
final Constructor<? extends Addon> constructor = addonClass.getConstructor();
final Addon addon = constructor.newInstance();
final Field gameField = addonClass.getSuperclass().getDeclaredField("game");
gameField.setAccessible(true);
gameField.set(addon, game);
gameField.setAccessible(false);
final Field descriptionField = addonClass.getSuperclass().getDeclaredField("description");
descriptionField.setAccessible(true);
descriptionField.set(addon, description);
descriptionField.setAccessible(false);
final Field loggerField = addonClass.getSuperclass().getDeclaredField("logger");
loggerField.setAccessible(true);
loggerField.set(addon, LogManager.getLogger(description.getName()));
loggerField.setAccessible(false);
final Field dataPathField = addonClass.getSuperclass().getDeclaredField("dataPath");
dataPathField.setAccessible(true);
dataPathField.set(addon, dataPath);
dataPathField.setAccessible(false);
addons.add(addon);
addonMD5s.put(description.getIdentifier(), FileUtils.getMD5Checksum(path));
if (game.getSide().isServer()) {
((CommonFileSystem) game.getFileSystem()).onAddonLoad(addon, path);
}
return addon;
} catch (Exception e) {
throw new InvalidAddonException(e);
}
}
@Override
public Collection<Addon> loadAddons(Path path) {
if (!Files.isDirectory(path)) {
throw new IllegalArgumentException("Path " + path + " is not a directory!");
}
final DirectoryStream<Path> stream;
try {
stream = Files.newDirectoryStream(path, new DirectoryStream.Filter<Path>() {
@Override
public boolean accept(Path entry) {
String fname = entry.getFileName().toString();
return !Files.isDirectory(entry) && fname.endsWith(".jar");
}
});
} catch (IOException e) {
throw new RuntimeException("IO error occurred while traversing addons dir", e);
}
for (Path jar : stream) {
try {
loadAddon(jar);
} catch (Exception e) {
game.getLogger().fatal("Unable to load [" + jar.getFileName() + "] in directory [" + path + "]", e);
}
}
return Collections.unmodifiableCollection(addons);
}
@Override
public void initialize(Addon addon) {
if (addon.isEnabled()) {
throw new IllegalStateException("Cannot initialize addon [" + addon.getDescription().getName() + "], it has already been initialized and enabled!");
}
game.getLogger().info("Initializing [" + addon.getDescription().getName() + " " + addon.getDescription().getVersion() + "]...");
try {
addon.onInitialize();
final Field initializedField = addon.getClass().getSuperclass().getDeclaredField("initialized");
initializedField.setAccessible(true);
initializedField.setBoolean(addon, true);
initializedField.setAccessible(false);
game.getLogger().info("[" + addon.getDescription().getName() + "] initialized");
} catch (Throwable t) {
game.getLogger().fatal("Exception caught while initializing addon [" + addon.getDescription().getName() + "]", t);
}
}
@Override
public void enable(Addon addon) {
if (!addon.isInitialized()) {
throw new IllegalStateException("Cannot enable addon [" + addon.getDescription().getName() + "], it has not been initialized!");
}
if (addon.isEnabled()) {
throw new IllegalStateException("Cannot enable addon [" + addon.getDescription().getName() + "], it has already been enabled!");
}
game.getLogger().info("Enabling [" + addon.getDescription().getName() + " " + addon.getDescription().getVersion() + "]...");
try {
addon.onEnable();
final Field enableField = addon.getClass().getSuperclass().getDeclaredField("enabled");
enableField.setAccessible(true);
enableField.setBoolean(addon, true);
enableField.setAccessible(false);
game.getLogger().info("[" + addon.getDescription().getName() + "] enabled");
} catch (Throwable t) {
game.getLogger().fatal("Exception caught while enabling addon [" + addon.getDescription().getName() + "]", t);
}
}
@Override
public void disable(Addon addon) {
if (!addon.isEnabled()) {
throw new IllegalStateException("Cannot disable addon [" + addon.getDescription().getName() + "], it has never been enabled!");
}
game.getLogger().info("Disabling [" + addon.getDescription().getName() + " " + addon.getDescription().getVersion() + "]...");
try {
addon.onDisable();
final Field enableField = addon.getClass().getSuperclass().getDeclaredField("enabled");
enableField.setAccessible(true);
enableField.setBoolean(addon, false);
enableField.setAccessible(false);
game.getLogger().info("[" + addon.getDescription().getName() + "] disabled");
} catch (Throwable t) {
game.getLogger().fatal("Exception caught while disabling addon [" + addon.getDescription().getName() + "]", t);
}
}
public static AddonDescription create(Path path) throws InvalidAddonException, InvalidDescriptionException {
if (!Files.exists(path)) {
throw new InvalidAddonException("Attempt to create addon description for file " + path.getFileName() + " but it does not exist!");
}
final AddonDescription description;
try (final JarFile jar = new JarFile(path.toFile())) {
final JarEntry entry = jar.getJarEntry(ADDON_JSON);
if (entry == null) {
throw new InvalidDescriptionException("Attempt to create an addon description failed because " + ADDON_JSON + " was not found in " + jar.getName());
}
final GsonBuilder builder = new GsonBuilder();
builder.registerTypeAdapter(AddonDescription.class, new AddonDescriptionJsonDeserializer());
final Gson gson = builder.create();
description = gson.fromJson(new InputStreamReader(jar.getInputStream(entry)), AddonDescription.class);
} catch (IOException e) {
throw new InvalidAddonException(e);
}
return description;
}
public void initialize() {
for (Addon addon : addons) {
initialize(addon);
}
}
public void disable() {
for (Addon addon : addons) {
disable(addon);
}
}
public void enable() {
for (Addon addon : addons) {
enable(addon);
}
}
public boolean isOfficialAddon(String identifier) {
return "internal".equalsIgnoreCase(identifier);
}
public Addon getInternalAddon() {
return internal;
}
public SerializableHashMap getAddonMD5s() {
return addonMD5s;
}
}