Implement WebManager that will download Challenges Libraries from GitHub.

Implement GUI for selecting and downloading Challenges Libraries.
This commit is contained in:
BONNe1704 2019-09-02 17:56:24 +03:00
parent 29a77147b5
commit f611727d4e
8 changed files with 790 additions and 2 deletions

View File

@ -21,6 +21,7 @@ import world.bentobox.challenges.config.Settings;
import world.bentobox.challenges.handlers.*;
import world.bentobox.challenges.listeners.ResetListener;
import world.bentobox.challenges.listeners.SaveListener;
import world.bentobox.challenges.web.WebManager;
import world.bentobox.level.Level;
@ -39,6 +40,11 @@ public class ChallengesAddon extends Addon {
private ChallengesImportManager importManager;
/**
* This class manages web content loading.
*/
private WebManager webManager;
private Settings settings;
private boolean hooked;
@ -138,6 +144,9 @@ public class ChallengesAddon extends Addon {
// Challenge import setup
this.importManager = new ChallengesImportManager(this);
// Web content loading
this.webManager = new WebManager(this);
List<GameModeAddon> hookedGameModes = new ArrayList<>();
this.getPlugin().getAddonsManager().getGameModeAddons().forEach(gameModeAddon -> {
@ -315,6 +324,14 @@ public class ChallengesAddon extends Addon {
}
/**
* @return the webManager
*/
public WebManager getWebManager() {
return this.webManager;
}
/**
* @return the challenge settings.
*/

View File

@ -126,6 +126,77 @@ public class ChallengesImportManager
}
/**
* This method loads downloaded challenges into memory.
* @param user User who calls downloaded challenge loading
* @param world Target world.
* @param downloadString String that need to be loaded via DefaultDataHolder.
* @return <code>true</code> if everything was successful, otherwise <code>false</code>.
*/
public boolean loadDownloadedChallenges(User user, World world, String downloadString)
{
ChallengesManager manager = this.addon.getChallengesManager();
// If exist any challenge or level that is bound to current world, then do not load default challenges.
if (manager.hasAnyChallengeData(world.getName()))
{
if (user.isPlayer())
{
user.sendMessage("challenges.errors.exist-challenges-or-levels");
}
else
{
this.addon.logWarning("challenges.errors.exist-challenges-or-levels");
}
return false;
}
try
{
// This prefix will be used to all challenges. That is a unique way how to separate challenged for
// each game mode.
String uniqueIDPrefix = Utils.getGameMode(world) + "_";
DefaultDataHolder downloadedChallenges = new DefaultJSONHandler(this.addon).loadWebObject(downloadString);
// All new challenges should get correct ID. So we need to map it to loaded challenges.
downloadedChallenges.getChallengeList().forEach(challenge -> {
// Set correct challenge ID
challenge.setUniqueId(uniqueIDPrefix + challenge.getUniqueId());
// Set up correct level ID if it is necessary
if (!challenge.getLevel().isEmpty())
{
challenge.setLevel(uniqueIDPrefix + challenge.getLevel());
}
// Load challenge in memory
manager.loadChallenge(challenge, false, user, user == null);
});
downloadedChallenges.getLevelList().forEach(challengeLevel -> {
// Set correct level ID
challengeLevel.setUniqueId(uniqueIDPrefix + challengeLevel.getUniqueId());
// Set correct world name
challengeLevel.setWorld(Util.getWorld(world).getName());
// Reset names for all challenges.
challengeLevel.setChallenges(challengeLevel.getChallenges().stream().
map(challenge -> uniqueIDPrefix + challenge).
collect(Collectors.toSet()));
// Load level in memory
manager.loadLevel(challengeLevel, false, user, user == null);
});
}
catch (Exception e)
{
e.printStackTrace();
return false;
}
this.addon.getChallengesManager().save();
return true;
}
// ---------------------------------------------------------------------
// Section: Default generation
// ---------------------------------------------------------------------
@ -320,6 +391,16 @@ public class ChallengesImportManager
}
/**
* This method creates and adds to list all objects from default.json file.
* @return List of all objects from default.json that is with T instance.
*/
DefaultDataHolder loadWebObject(String downloadedObject)
{
return this.gson.fromJson(downloadedObject, DefaultDataHolder.class);
}
// ---------------------------------------------------------------------
// Section: Variables
// ---------------------------------------------------------------------

View File

@ -53,7 +53,7 @@ public abstract class CommonGUI
/**
* This variable stores parent gui.
*/
private CommonGUI parentGUI;
protected CommonGUI parentGUI;
/**
* Variable stores Challenges addon.
@ -136,6 +136,7 @@ public abstract class CommonGUI
protected static final String COMPLETE = "complete";
protected static final String DOWNLOAD = "download";
// ---------------------------------------------------------------------
// Section: Constructors
@ -205,6 +206,40 @@ public abstract class CommonGUI
}
/**
* Default constructor that inits panels with minimal requirements.
* @param parentGUI Parent panel for current panel.
*/
public CommonGUI(CommonGUI parentGUI)
{
this.addon = parentGUI.addon;
this.world = parentGUI.world;
this.user = parentGUI.user;
this.topLabel = parentGUI.topLabel;
this.permissionPrefix = parentGUI.permissionPrefix;
this.parentGUI = parentGUI;
this.pageIndex = 0;
this.returnButton = new PanelItemBuilder().
name(this.user.getTranslation("challenges.gui.buttons.return")).
icon(Material.OAK_DOOR).
clickHandler((panel, user1, clickType, i) -> {
if (this.parentGUI == null)
{
this.user.closeInventory();
return true;
}
this.parentGUI.build();
return true;
}).build();
}
// ---------------------------------------------------------------------
// Section: Common methods
// ---------------------------------------------------------------------

View File

@ -63,7 +63,8 @@ public class AdminGUI extends CommonGUI
EDIT_SETTINGS,
DEFAULT_IMPORT_CHALLENGES,
DEFAULT_EXPORT_CHALLENGES,
COMPLETE_WIPE
COMPLETE_WIPE,
LIBRARY
}
@ -122,6 +123,8 @@ public class AdminGUI extends CommonGUI
// Import Challenges
panelBuilder.item(15, this.createButton(Button.DEFAULT_IMPORT_CHALLENGES));
panelBuilder.item(24, this.createButton(Button.LIBRARY));
// Not added as I do not think admins should use it. It still will be able via command.
// panelBuilder.item(33, this.createButton(Button.DEFAULT_EXPORT_CHALLENGES));
@ -439,6 +442,21 @@ public class AdminGUI extends CommonGUI
break;
}
case LIBRARY:
{
permissionSuffix = DOWNLOAD;
name = this.user.getTranslation("challenges.gui.buttons.admin.library");
description = this.user.getTranslation("challenges.gui.descriptions.admin.library");
icon = new ItemStack(Material.COBWEB);
clickHandler = (panel, user, clickType, slot) -> {
ListLibraryGUI.open(this);
return true;
};
glow = false;
break;
}
default:
// This should never happen.
return null;

View File

@ -0,0 +1,212 @@
package world.bentobox.challenges.panel.admin;
import org.bukkit.ChatColor;
import org.bukkit.World;
import java.util.ArrayList;
import java.util.List;
import world.bentobox.bentobox.api.panels.PanelItem;
import world.bentobox.bentobox.api.panels.builders.PanelBuilder;
import world.bentobox.bentobox.api.panels.builders.PanelItemBuilder;
import world.bentobox.bentobox.api.user.User;
import world.bentobox.challenges.ChallengesAddon;
import world.bentobox.challenges.panel.CommonGUI;
import world.bentobox.challenges.utils.GuiUtils;
import world.bentobox.challenges.web.object.LibraryEntry;
/**
* This class contains all necessary elements to create GUI that lists all challenges.
* It allows to edit them or remove, depending on given input mode.
*/
public class ListLibraryGUI extends CommonGUI
{
// ---------------------------------------------------------------------
// Section: Constructor
// ---------------------------------------------------------------------
/**
* @param parentGUI ParentGUI object.
*/
public ListLibraryGUI(CommonGUI parentGUI)
{
super(parentGUI);
}
/**
* @param addon Addon where panel operates.
* @param world World from which panel was created.
* @param user User who created panel.
* @param topLabel Command top label which creates panel (f.e. island or ai)
* @param permissionPrefix Command permission prefix (f.e. bskyblock.)
*/
public ListLibraryGUI(ChallengesAddon addon,
World world,
User user,
String topLabel,
String permissionPrefix)
{
super(addon, world, user, topLabel, permissionPrefix, null);
}
/**
* This static method allows to easier open Library GUI.
* @param parentGui ParentGUI object.
*/
public static void open(CommonGUI parentGui)
{
new ListLibraryGUI(parentGui).build();
}
// ---------------------------------------------------------------------
// Section: Methods
// ---------------------------------------------------------------------
/**
* {@inheritDoc}
*/
@Override
public void build()
{
PanelBuilder panelBuilder = new PanelBuilder().user(this.user).name(
this.user.getTranslation("challenges.gui.title.admin.library-title"));
GuiUtils.fillBorder(panelBuilder);
List<LibraryEntry> libraryEntries = this.addon.getWebManager().getLibraryEntries();
final int MAX_ELEMENTS = 21;
if (this.pageIndex < 0)
{
this.pageIndex = libraryEntries.size() / MAX_ELEMENTS;
}
else if (this.pageIndex > (libraryEntries.size() / MAX_ELEMENTS))
{
this.pageIndex = 0;
}
int entryIndex = MAX_ELEMENTS * this.pageIndex;
// I want first row to be only for navigation and return button.
int index = 10;
while (entryIndex < ((this.pageIndex + 1) * MAX_ELEMENTS) &&
entryIndex < libraryEntries.size() &&
index < 36)
{
if (!panelBuilder.slotOccupied(index))
{
panelBuilder.item(index, this.createEntryIcon(libraryEntries.get(entryIndex++)));
}
index++;
}
// Navigation buttons only if necessary
if (libraryEntries.size() > MAX_ELEMENTS)
{
panelBuilder.item(18, this.getButton(CommonButtons.PREVIOUS));
panelBuilder.item(26, this.getButton(CommonButtons.NEXT));
}
panelBuilder.item(44, this.returnButton);
panelBuilder.build();
}
/**
* This method creates button for given library entry.
* @param libraryEntry LibraryEntry which button must be created.
* @return Entry button.
*/
private PanelItem createEntryIcon(LibraryEntry libraryEntry)
{
PanelItemBuilder itemBuilder = new PanelItemBuilder().
name(ChatColor.translateAlternateColorCodes('&', libraryEntry.getName())).
description(this.generateEntryDescription(libraryEntry)).
icon(libraryEntry.getIcon()).
glow(false);
itemBuilder.clickHandler((panel, user1, clickType, i) -> {
if (!this.blockedForDownland)
{
this.blockedForDownland = true;
this.user.sendMessage("challenges.messages.admin.start-downloading");
// Run download task after 5 ticks.
this.addon.getPlugin().getServer().getScheduler().
runTaskLaterAsynchronously(
this.addon.getPlugin(),
() -> this.addon.getWebManager().requestEntryGitHubData(this.user, this.world, libraryEntry),
5L);
if (this.parentGUI != null)
{
this.parentGUI.build();
}
else
{
this.user.closeInventory();
}
}
return true;
});
return itemBuilder.build();
}
/**
* This method generated description for LibraryEntry object.
* @param entry LibraryEntry object which description must be generated.
* @return List of strings that will be placed in ItemStack lore message.
*/
private List<String> generateEntryDescription(LibraryEntry entry)
{
List<String> description = new ArrayList<>();
description.add(this.user.getTranslation(REFERENCE_DESCRIPTION + "library-author",
"[author]",
entry.getAuthor()));
description.add(entry.getDescription());
description.add(this.user.getTranslation(REFERENCE_DESCRIPTION + "library-gamemode",
"[gamemode]",
entry.getForGameMode()));
description.add(this.user.getTranslation(REFERENCE_DESCRIPTION + "library-lang",
"[lang]",
entry.getLanguage()));
description.add(this.user.getTranslation(REFERENCE_DESCRIPTION + "library-version",
"[version]",
entry.getVersion()));
return GuiUtils.stringSplit(description,
this.addon.getChallengesSettings().getLoreLineLength());
}
// ---------------------------------------------------------------------
// Section: Instance Variables
// ---------------------------------------------------------------------
/**
* This variable will protect against spam-click.
*/
private boolean blockedForDownland;
/**
* Reference string to description.
*/
private static final String REFERENCE_DESCRIPTION = "challenges.gui.descriptions.admin.";
}

View File

@ -0,0 +1,225 @@
package world.bentobox.challenges.web;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import org.bukkit.World;
import java.nio.charset.StandardCharsets;
import java.util.*;
import io.github.TheBusyBiscuit.GitHubWebAPI4Java.GitHubWebAPI;
import io.github.TheBusyBiscuit.GitHubWebAPI4Java.objects.repositories.GitHubRepository;
import world.bentobox.bentobox.BentoBox;
import world.bentobox.bentobox.api.user.User;
import world.bentobox.challenges.ChallengesAddon;
import world.bentobox.challenges.web.object.LibraryEntry;
/**
* This class manages content downloading from web repository.
*/
public class WebManager
{
/**
* Default constructor
* @param addon Challenges Addon object.
*/
public WebManager(ChallengesAddon addon)
{
this.addon = addon;
this.plugin = addon.getPlugin();
this.library = new ArrayList<>(0);
if (this.plugin.getSettings().isGithubDownloadData())
{
long connectionInterval = this.plugin.getSettings().getGithubConnectionInterval() * 20L * 60L;
if (connectionInterval <= 0)
{
// If below 0, it means we shouldn't run this as a repeating task.
this.plugin.getServer().getScheduler().runTaskLaterAsynchronously(this.plugin,
() -> this.requestCatalogGitHubData(true),
20L);
}
else
{
// Set connection interval to be at least 60 minutes.
connectionInterval = Math.max(connectionInterval, 60 * 20 * 60L);
this.plugin.getServer().getScheduler().runTaskTimerAsynchronously(this.plugin,
() -> this.requestCatalogGitHubData(true),
20L,
connectionInterval);
}
}
}
/**
* This method requests catalog entries from challenges library.
* @param clearCache Boolean that indicates if all cached values must be cleared.
*/
public void requestCatalogGitHubData(boolean clearCache)
{
this.plugin.getWebManager().getGitHub().ifPresent(gh ->
{
if (this.plugin.getSettings().isLogGithubDownloadData())
{
this.plugin.log("Downloading data from GitHub...");
}
GitHubRepository repo = new GitHubRepository(gh, "BentoBoxWorld/weblink");
String catalogContent = "";
// Downloading the data
try
{
catalogContent = repo.getContent("challenges/catalog.json").getContent().replaceAll("\\n", "");
}
catch (IllegalAccessException e)
{
if (this.plugin.getSettings().isLogGithubDownloadData())
{
this.plugin.log("Could not connect to GitHub.");
}
}
catch (Exception e)
{
this.plugin.logError("An error occurred when downloading data from GitHub...");
this.plugin.logStacktrace(e);
}
// People were concerned that the download took ages, so we need to tell them it's over now.
if (this.plugin.getSettings().isLogGithubDownloadData())
{
this.plugin.log("Successfully downloaded data from GitHub.");
}
// Decoding the Base64 encoded contents
catalogContent = new String(Base64.getDecoder().decode(catalogContent),
StandardCharsets.UTF_8);
/* Parsing the data */
// Register the catalog data
if (!catalogContent.isEmpty())
{
if (clearCache)
{
this.library.clear();
}
JsonObject catalog = new JsonParser().parse(catalogContent).getAsJsonObject();
catalog.getAsJsonArray("challenges").forEach(gamemode ->
this.library.add(new LibraryEntry(gamemode.getAsJsonObject())));
}
});
}
/**
* This method requests GitHub data for given LibraryEntry object.
* @param user User who inits request.
* @param world Target world where challenges should be loaded.
* @param entry Entry that contains information about requested object.
* @return {@code true} if request was successful, {@code false} otherwise.
*/
public boolean requestEntryGitHubData(User user, World world, LibraryEntry entry)
{
Optional<GitHubWebAPI> gitAPI = this.plugin.getWebManager().getGitHub();
if (gitAPI.isPresent())
{
if (this.plugin.getSettings().isLogGithubDownloadData())
{
this.plugin.log("Downloading data from GitHub...");
}
GitHubRepository repo = new GitHubRepository(gitAPI.get(), "BentoBoxWorld/weblink");
String challengeLibrary = "";
// Downloading the data
try
{
challengeLibrary = repo.getContent("challenges/library/" + entry.getRepository() + ".json").
getContent().
replaceAll("\\n", "");
}
catch (IllegalAccessException e)
{
if (this.plugin.getSettings().isLogGithubDownloadData())
{
this.plugin.log("Could not connect to GitHub.");
}
}
catch (Exception e)
{
this.plugin.logError("An error occurred when downloading data from GitHub...");
this.plugin.logStacktrace(e);
}
// People were concerned that the download took ages, so we need to tell them it's over now.
if (this.plugin.getSettings().isLogGithubDownloadData())
{
this.plugin.log("Successfully downloaded data from GitHub.");
}
// Decoding the Base64 encoded contents
challengeLibrary = new String(Base64.getDecoder().decode(challengeLibrary),
StandardCharsets.UTF_8);
/* Parsing the data */
// Process downloaded library data
if (!challengeLibrary.isEmpty())
{
this.addon.getImportManager().loadDownloadedChallenges(user, world, challengeLibrary);
return true;
}
}
return false;
}
// ---------------------------------------------------------------------
// Section: Getters
// ---------------------------------------------------------------------
/**
* This method returns all library entries that are downlaoded.
* @return existing Library entries.
*/
public List<LibraryEntry> getLibraryEntries()
{
List<LibraryEntry> entries = new ArrayList<>(this.library);
entries.sort(Comparator.comparingInt(LibraryEntry::getSlot));
return entries;
}
// ---------------------------------------------------------------------
// Section: Variables
// ---------------------------------------------------------------------
/**
* Challenges Addon variable.
*/
private ChallengesAddon addon;
/**
* BentoBox plugin variable.
*/
private BentoBox plugin;
/**
* This list contains all entries that were downloaded from GitHub.
*/
private List<LibraryEntry> library;
}

View File

@ -0,0 +1,186 @@
package world.bentobox.challenges.web.object;
import com.google.gson.JsonObject;
import org.bukkit.Material;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.Nullable;
/**
* This objects allows to load each Challenges Catalog library entry.
*/
public class LibraryEntry
{
/**
* Default constructor.
* @param object Json Object that must be translated to LibraryEntry.
*/
public LibraryEntry(@NonNull JsonObject object)
{
this.name = object.get("name").getAsString();
Material material = Material.matchMaterial(object.get("icon").getAsString());
this.icon = (material != null) ? material : Material.PAPER;
this.description = object.get("description").getAsString();
this.repository = object.get("repository").getAsString();
this.language = object.get("language").getAsString();
this.slot = object.get("slot").getAsInt();
this.forGameMode = object.get("for").getAsString();
this.author = object.get("author").getAsString();
this.version = object.get("version").getAsString();
}
/**
* This method returns the name value.
* @return the value of name.
*/
@NonNull
public String getName()
{
return name;
}
/**
* This method returns the icon value.
* @return the value of icon.
*/
@NonNull
public Material getIcon()
{
return icon;
}
/**
* This method returns the description value.
* @return the value of description.
*/
@NonNull
public String getDescription()
{
return description;
}
/**
* This method returns the repository value.
* @return the value of repository.
*/
@NonNull
public String getRepository()
{
return repository;
}
/**
* This method returns the language value.
* @return the value of language.
*/
@NonNull
public String getLanguage()
{
return language;
}
/**
* This method returns the slot value.
* @return the value of slot.
*/
@NonNull
public int getSlot()
{
return slot;
}
/**
* This method returns the forGameMode value.
* @return the value of forGameMode.
*/
@NonNull
public String getForGameMode()
{
return forGameMode;
}
/**
* This method returns the author value.
* @return the value of author.
*/
@NonNull
public String getAuthor()
{
return author;
}
/**
* This method returns the version value.
* @return the value of version.
*/
@NonNull
public String getVersion()
{
return version;
}
// ---------------------------------------------------------------------
// Section: Variables
// ---------------------------------------------------------------------
/**
* Name of entry object
*/
private @NonNull String name;
/**
* Defaults to {@link Material#PAPER}.
*/
private @NonNull Material icon;
/**
* Description of entry object.
*/
private @NonNull String description;
/**
* File name in challenges library.
*/
private @NonNull String repository;
/**
* Language of content.
*/
private @Nullable String language;
/**
* Desired slot number.
*/
private int slot;
/**
* Main GameMode for which challenges were created.
*/
private @Nullable String forGameMode;
/**
* Author (-s) who created current configuration.
*/
private @Nullable String author;
/**
* Version of Challenges Addon, for which challenges were created.
*/
private @Nullable String version;
}

View File

@ -67,6 +67,8 @@ challenges:
select-entity: '&aSelect Entity'
toggle-environment: '&aToggle Environment'
edit-text-fields: '&aEdit Text Fields'
library-title: '&aDownloadable Libraries'
challenges: '&6Challenges'
game-modes: '&6Choose GameMode'
@ -161,6 +163,8 @@ challenges:
default-import: 'Import Default Challenges'
default-export: 'Export Existing Challenges'
complete-wipe: 'Wipe Addon Databases'
library: 'Web Library'
next: 'Next'
previous: 'Previous'
return: 'Return'
@ -261,6 +265,14 @@ challenges:
default-import: 'Allows to import default challenges.'
default-export: 'Allows to export existing challenges into defaults.json file.'
complete-wipe: 'Allows to completely clear all challenges addon databases. Includes player data!'
library: 'Opens GUI that shows all available public Challenges Libraries.'
library-author: 'by &e[author]'
library-version: '&9Made on Challenges [version]'
library-lang: '&aLanguage: [lang]'
library-gamemode: '&aPrimary for [gamemode]'
current-value: '|&6Current value: [value].'
enabled: 'Active'
disabled: 'Disabled'
@ -368,6 +380,8 @@ challenges:
migrate-start: '&2Start migrating challenges addon data.'
migrate-end: '&2Challenges addon data is updated to new format.'
migrate-not: '&2All data is valid.'
start-downloading: '&5Starting to download and import Challenges Library.'
you-completed-challenge: '&2You completed the [value] &r&2challenge!'
you-repeated-challenge: '&2You repeated the [value] &r&2challenge!'
you-repeated-challenge-multiple: '&2You repeated the [value] &r&2challenge [count] times!'