diff --git a/src/main/java/world/bentobox/bentobox/database/objects/Island.java b/src/main/java/world/bentobox/bentobox/database/objects/Island.java index 2f19c9ff2..7f26e3180 100644 --- a/src/main/java/world/bentobox/bentobox/database/objects/Island.java +++ b/src/main/java/world/bentobox/bentobox/database/objects/Island.java @@ -1044,9 +1044,11 @@ public class Island implements DataObject, MetaDataAble { * * @param flag - flag * @param value - Use RanksManager settings, e.g. RanksManager.MEMBER + * @return this island */ - public void setFlag(Flag flag, int value) { + public Island setFlag(Flag flag, int value) { setFlag(flag, value, true); + return this; } /** @@ -1079,9 +1081,10 @@ public class Island implements DataObject, MetaDataAble { /** * Resets the flags to their default as set in config.yml for this island. If * flags are missing from the config, the default hard-coded value is used and - * set + * set. + * @return this island */ - public void setFlagsDefaults() { + public Island setFlagsDefaults() { BentoBox plugin = BentoBox.getInstance(); Map result = new HashMap<>(); plugin.getFlagsManager().getFlags().stream().filter(f -> f.getType().equals(Flag.Type.PROTECTION)) @@ -1090,7 +1093,8 @@ public class Island implements DataObject, MetaDataAble { plugin.getFlagsManager().getFlags().stream().filter(f -> f.getType().equals(Flag.Type.SETTING)) .forEach(f -> result.put(f.getID(), plugin.getIWM().getDefaultIslandSettings(world).getOrDefault(f, f.getDefaultRank()))); - this.setFlags(result); + setFlags(result); + return this; } /** diff --git a/src/main/java/world/bentobox/bentobox/managers/IslandsManager.java b/src/main/java/world/bentobox/bentobox/managers/IslandsManager.java index 98cdd2269..ed9ed6131 100644 --- a/src/main/java/world/bentobox/bentobox/managers/IslandsManager.java +++ b/src/main/java/world/bentobox/bentobox/managers/IslandsManager.java @@ -1455,7 +1455,7 @@ public class IslandsManager { */ public void saveAll(boolean schedule) { if (!schedule) { - for (Island island : islandCache.getIslands()) { + for (Island island : islandCache.getCachedIslands()) { if (island.isChanged()) { try { handler.saveObjectAsync(island); @@ -1468,7 +1468,7 @@ public class IslandsManager { } isSaveTaskRunning = true; - Queue queue = new LinkedList<>(islandCache.getIslands()); + Queue queue = new LinkedList<>(islandCache.getCachedIslands()); new BukkitRunnable() { @Override public void run() { diff --git a/src/main/java/world/bentobox/bentobox/managers/island/IslandCache.java b/src/main/java/world/bentobox/bentobox/managers/island/IslandCache.java index 67a8b4f50..0a5688598 100644 --- a/src/main/java/world/bentobox/bentobox/managers/island/IslandCache.java +++ b/src/main/java/world/bentobox/bentobox/managers/island/IslandCache.java @@ -9,11 +9,13 @@ import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.Objects; import java.util.Set; import java.util.UUID; import java.util.stream.Collectors; +import org.bukkit.Bukkit; import org.bukkit.Location; import org.bukkit.World; import org.eclipse.jdt.annotation.NonNull; @@ -104,7 +106,8 @@ public class IslandCache { return false; } if (addToGrid(island)) { - setIslandById(island); + // Insert a null into the map as a placeholder for cache + islandsById.put(island.getUniqueId().intern(), null); // Only add islands to this map if they are owned if (island.isOwned()) { islandsByUUID.computeIfAbsent(island.getOwner(), k -> new HashSet<>()).add(island.getUniqueId()); @@ -253,18 +256,40 @@ public class IslandCache { /** * Returns an unmodifiable collection of all the islands (even - * those who may be unowned). + * those who may be unowned). Gets them from the cache or from the database if not + * loaded. * * @return unmodifiable collection containing every island. */ @NonNull public Collection getIslands() { - return Collections.unmodifiableCollection(islandsById.values()); + List result = new ArrayList<>(); + for (Entry<@NonNull String, @NonNull Island> entry : islandsById.entrySet()) { + Island island = entry.getValue() != null ? entry.getValue() : handler.loadObject(entry.getKey()); + if (island != null) { + result.add(island); + } + } + + return Collections.unmodifiableCollection(result); } /** * Returns an unmodifiable collection of all the islands (even - * those who may be unowned) in the specified world. + * those who may be unowned) that are cached. + * + * @return unmodifiable collection containing every cached island. + */ + @NonNull + public Collection getCachedIslands() { + return islandsById.entrySet().stream().filter(en -> Objects.nonNull(en.getValue())).map(Map.Entry::getValue) + .toList(); + } + + /** + * Returns an unmodifiable collection of all the islands (even + * those that may be unowned) in the specified world. + * Gets islands from the cache if they have been loaded, or from the database if not * * @param world World of the gamemode. * @return unmodifiable collection containing all the islands in the specified @@ -277,9 +302,16 @@ public class IslandCache { if (overworld == null) { return Collections.emptyList(); } - return islandsById.entrySet().stream() - .filter(entry -> overworld.equals(Util.getWorld(entry.getValue().getWorld()))) // shouldn't make NPEs - .map(Map.Entry::getValue).toList(); + + List result = new ArrayList<>(); + for (Entry<@NonNull String, @NonNull Island> entry : islandsById.entrySet()) { + Island island = entry.getValue() != null ? entry.getValue() : handler.loadObject(entry.getKey()); + if (island != null && overworld.equals(island.getWorld())) { + result.add(island); + } + } + + return Collections.unmodifiableCollection(result); } /** @@ -294,7 +326,7 @@ public class IslandCache { if (!islandsByUUID.containsKey(uuid)) { return false; } - return this.islandsByUUID.get(uuid).stream().map(islandsById::get).filter(Objects::nonNull) + return this.islandsByUUID.get(uuid).stream().map(this::getIslandById).filter(Objects::nonNull) .filter(i -> world.equals(i.getWorld())) .anyMatch(i -> uuid.equals(i.getOwner())); } @@ -364,7 +396,7 @@ public class IslandCache { } /** - * Gets the number of islands in the cache for this world + * Gets the number of islands in this world * * @param world world to get the number of islands in * @return the number of islands @@ -411,17 +443,14 @@ public class IslandCache { } /** - * Resets all islands in this game mode to default flag settings + * Resets all islands in this game mode to default flag settings. * * @param world - world * @since 1.3.0 */ public void resetAllFlags(World world) { - World w = Util.getWorld(world); - if (w == null) { - return; - } - islandsById.values().stream().filter(i -> i.getWorld().equals(w)).forEach(Island::setFlagsDefaults); + Bukkit.getScheduler().runTaskAsynchronously(BentoBox.getInstance(), + () -> this.getIslands(world).stream().forEach(Island::setFlagsDefaults)); } /** @@ -432,13 +461,9 @@ public class IslandCache { * @since 1.8.0 */ public void resetFlag(World world, Flag flag) { - World w = Util.getWorld(world); - if (w == null) { - return; - } - int setting = BentoBox.getInstance().getIWM().getDefaultIslandFlags(w).getOrDefault(flag, + int setting = BentoBox.getInstance().getIWM().getDefaultIslandFlags(world).getOrDefault(flag, flag.getDefaultRank()); - islandsById.values().stream().filter(i -> i.getWorld().equals(w)).forEach(i -> i.setFlag(flag, setting)); + this.getIslands(world).stream().forEach(i -> i.setFlag(flag, setting)); } /** diff --git a/src/test/java/world/bentobox/bentobox/managers/IslandsManagerTest.java b/src/test/java/world/bentobox/bentobox/managers/IslandsManagerTest.java index 4c46d2613..87e6f4bc3 100644 --- a/src/test/java/world/bentobox/bentobox/managers/IslandsManagerTest.java +++ b/src/test/java/world/bentobox/bentobox/managers/IslandsManagerTest.java @@ -13,8 +13,10 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import java.beans.IntrospectionException; import java.io.File; import java.io.IOException; +import java.lang.reflect.InvocationTargetException; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; @@ -29,6 +31,7 @@ import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.UUID; +import java.util.concurrent.CompletableFuture; import org.bukkit.Bukkit; import org.bukkit.Chunk; @@ -54,6 +57,7 @@ import org.bukkit.plugin.PluginManager; import org.bukkit.scheduler.BukkitScheduler; import org.junit.After; import org.junit.Before; +import org.junit.BeforeClass; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; @@ -77,7 +81,9 @@ import world.bentobox.bentobox.Settings; import world.bentobox.bentobox.api.configuration.WorldSettings; import world.bentobox.bentobox.api.events.island.IslandDeleteEvent; import world.bentobox.bentobox.api.user.User; +import world.bentobox.bentobox.database.AbstractDatabaseHandler; import world.bentobox.bentobox.database.Database; +import world.bentobox.bentobox.database.DatabaseSetup; import world.bentobox.bentobox.database.DatabaseSetup.DatabaseType; import world.bentobox.bentobox.database.objects.Island; import world.bentobox.bentobox.listeners.flags.AbstractCommonSetup; @@ -86,9 +92,10 @@ import world.bentobox.bentobox.managers.island.IslandCache; import world.bentobox.bentobox.util.Util; @RunWith(PowerMockRunner.class) -@PrepareForTest({ Bukkit.class, BentoBox.class, Util.class, Location.class, MultiLib.class }) +@PrepareForTest({ Bukkit.class, BentoBox.class, Util.class, Location.class, MultiLib.class, DatabaseSetup.class, }) public class IslandsManagerTest extends AbstractCommonSetup { + private static AbstractDatabaseHandler h; @Mock private BentoBox plugin; private UUID uuid; @@ -146,6 +153,20 @@ public class IslandsManagerTest extends AbstractCommonSetup { // Class under test IslandsManager im; + @SuppressWarnings("unchecked") + @BeforeClass + public static void beforeClass() throws IllegalAccessException, InvocationTargetException, IntrospectionException { + // This has to be done beforeClass otherwise the tests will interfere with each + // other + h = mock(AbstractDatabaseHandler.class); + // Database + PowerMockito.mockStatic(DatabaseSetup.class); + DatabaseSetup dbSetup = mock(DatabaseSetup.class); + when(DatabaseSetup.getDatabase()).thenReturn(dbSetup); + when(dbSetup.getHandler(any())).thenReturn(h); + when(h.saveObject(any())).thenReturn(CompletableFuture.completedFuture(true)); + } + @Override @SuppressWarnings("unchecked") @Before @@ -603,23 +624,39 @@ public class IslandsManagerTest extends AbstractCommonSetup { /** * Test method for * {@link world.bentobox.bentobox.managers.IslandsManager#getIsland(World, User)} + * @throws IntrospectionException + * @throws NoSuchMethodException + * @throws ClassNotFoundException + * @throws InvocationTargetException + * @throws IllegalAccessException + * @throws InstantiationException */ @Test - public void testGetIslandWorldUser() { - Island island = im.createIsland(location, user.getUniqueId()); - assertEquals(island, im.getIsland(world, user)); + public void testGetIslandWorldUser() throws InstantiationException, IllegalAccessException, + InvocationTargetException, ClassNotFoundException, NoSuchMethodException, IntrospectionException { + Island is = im.createIsland(location, user.getUniqueId()); + when(h.loadObject(anyString())).thenReturn(is); + assertEquals(is, im.getIsland(world, user)); assertNull(im.getIsland(world, (User) null)); } /** * Test method for * {@link world.bentobox.bentobox.managers.IslandsManager#getIsland(World, UUID)}. + * @throws IntrospectionException + * @throws NoSuchMethodException + * @throws ClassNotFoundException + * @throws InvocationTargetException + * @throws IllegalAccessException + * @throws InstantiationException */ @Test - public void testGetIsland() { + public void testGetIsland() throws InstantiationException, IllegalAccessException, InvocationTargetException, + ClassNotFoundException, NoSuchMethodException, IntrospectionException { UUID owner = UUID.randomUUID(); - Island island = im.createIsland(location, owner); - assertEquals(island, im.getIsland(world, owner)); + Island is = im.createIsland(location, owner); + when(h.loadObject(anyString())).thenReturn(is); + assertEquals(is, im.getIsland(world, owner)); assertNull(im.getIsland(world, UUID.randomUUID())); } @@ -647,12 +684,21 @@ public class IslandsManagerTest extends AbstractCommonSetup { /** * Test method for * {@link world.bentobox.bentobox.managers.IslandsManager#getIslandLocation(World, UUID)}. + * @throws IntrospectionException + * @throws NoSuchMethodException + * @throws ClassNotFoundException + * @throws InvocationTargetException + * @throws IllegalAccessException + * @throws InstantiationException */ @Test - public void testGetIslandLocation() { - Island i = im.createIsland(location, uuid); + public void testGetIslandLocation() throws InstantiationException, IllegalAccessException, + InvocationTargetException, ClassNotFoundException, NoSuchMethodException, IntrospectionException { + // Store island in database + when(h.loadObject(anyString())).thenReturn(island); + im.createIsland(location, uuid); assertEquals(world, im.getIslandLocation(world, uuid).getWorld()); - assertEquals(i.getProtectionCenter(), im.getIslandLocation(world, uuid)); + assertEquals(location, im.getIslandLocation(world, uuid)); assertNull(im.getIslandLocation(world, UUID.randomUUID())); } @@ -719,41 +765,6 @@ public class IslandsManagerTest extends AbstractCommonSetup { assertEquals(Optional.empty(), im.getProtectedIslandAt(location)); } - /** - * Test method for - * {@link world.bentobox.bentobox.managers.IslandsManager#getSafeHomeLocation(World, User, int)}. - */ - /* - * @Test public void testGetSafeHomeLocation() { im.setIslandCache(islandCache); - * when(island.getHome(any())).thenReturn(location); - * when(iwm.inWorld(eq(world))).thenReturn(true); assertEquals(location, - * im.getSafeHomeLocation(world, user, "")); - * - * // Change location so that it is not safe // TODO } - */ - - /** - * Test method for - * {@link world.bentobox.bentobox.managers.IslandsManager#getSafeHomeLocation(World, User, int)}. - * Ensures that the method returns {@code null} if the world is not an island - * world. - */ - /* - * @Test public void testGetSafeHomeLocationWorldNotIslandWorld() { - * when(iwm.inWorld(world)).thenReturn(false); - * assertNull(im.getSafeHomeLocation(world, user, "")); } - */ - - /** - * Test method for - * {@link world.bentobox.bentobox.managers.IslandsManager#getSafeHomeLocation(World, User, int)}. - */ - /* - * @Test public void testGetSafeHomeLocationNoIsland() { - * assertNull(im.getSafeHomeLocation(world, user, "")); - * verify(plugin).logWarning(eq("null player has no island in world world!")); } - */ - /** * Test method for * {@link world.bentobox.bentobox.managers.IslandsManager#getSpawnPoint(World)}. diff --git a/src/test/java/world/bentobox/bentobox/managers/PlayersManagerTest.java b/src/test/java/world/bentobox/bentobox/managers/PlayersManagerTest.java index 105264fed..973a2ac07 100644 --- a/src/test/java/world/bentobox/bentobox/managers/PlayersManagerTest.java +++ b/src/test/java/world/bentobox/bentobox/managers/PlayersManagerTest.java @@ -74,7 +74,7 @@ import world.bentobox.bentobox.util.Util; @PrepareForTest({ Bukkit.class, BentoBox.class, User.class, Util.class, Logger.class, DatabaseSetup.class, }) public class PlayersManagerTest { - private static AbstractDatabaseHandler h; + private static AbstractDatabaseHandler handler; private Database db; @Mock private World end; @@ -110,13 +110,13 @@ public class PlayersManagerTest { public static void beforeClass() throws IllegalAccessException, InvocationTargetException, IntrospectionException { // This has to be done beforeClass otherwise the tests will interfere with each // other - h = mock(AbstractDatabaseHandler.class); + handler = mock(AbstractDatabaseHandler.class); // Database PowerMockito.mockStatic(DatabaseSetup.class); DatabaseSetup dbSetup = mock(DatabaseSetup.class); when(DatabaseSetup.getDatabase()).thenReturn(dbSetup); - when(dbSetup.getHandler(any())).thenReturn(h); - when(h.saveObject(any())).thenReturn(CompletableFuture.completedFuture(true)); + when(dbSetup.getHandler(any())).thenReturn(handler); + when(handler.saveObject(any())).thenReturn(CompletableFuture.completedFuture(true)); } private void deleteAll(File file) throws IOException { @@ -244,15 +244,15 @@ public class PlayersManagerTest { // Loading objects Object players = new Players(); - when(h.loadObject(anyString())).thenReturn(players); + when(handler.loadObject(anyString())).thenReturn(players); // Set up names database List names = new ArrayList<>(); Names name = new Names(); name.setUniqueId("tastybento"); name.setUuid(uuid); names.add(name); - when(h.loadObjects()).thenReturn(names); - when(h.objectExists(anyString())).thenReturn(true); + when(handler.loadObjects()).thenReturn(names); + when(handler.objectExists(anyString())).thenReturn(true); // Class under test pm = new PlayersManager(plugin); @@ -637,7 +637,7 @@ public class PlayersManagerTest { public void testSetPlayerName() throws IllegalAccessException, InvocationTargetException, IntrospectionException { pm.setPlayerName(user); // Player and names database saves - verify(h, atLeast(2)).saveObject(any()); + verify(handler, atLeast(2)).saveObject(any()); } /** diff --git a/src/test/java/world/bentobox/bentobox/managers/island/IslandCacheTest.java b/src/test/java/world/bentobox/bentobox/managers/island/IslandCacheTest.java index 7a4c9c5a7..04cb50526 100644 --- a/src/test/java/world/bentobox/bentobox/managers/island/IslandCacheTest.java +++ b/src/test/java/world/bentobox/bentobox/managers/island/IslandCacheTest.java @@ -15,14 +15,25 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import java.beans.IntrospectionException; +import java.io.File; +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.Collections; +import java.util.Comparator; import java.util.Map; import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import org.bukkit.Bukkit; import org.bukkit.Location; import org.bukkit.World; +import org.bukkit.scheduler.BukkitScheduler; import org.junit.After; import org.junit.Before; +import org.junit.BeforeClass; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; @@ -37,17 +48,20 @@ import com.google.common.collect.ImmutableSet.Builder; import world.bentobox.bentobox.BentoBox; import world.bentobox.bentobox.api.flags.Flag; +import world.bentobox.bentobox.database.AbstractDatabaseHandler; import world.bentobox.bentobox.database.Database; +import world.bentobox.bentobox.database.DatabaseSetup; import world.bentobox.bentobox.database.objects.Island; +import world.bentobox.bentobox.listeners.flags.AbstractCommonSetup; import world.bentobox.bentobox.managers.IslandWorldManager; import world.bentobox.bentobox.managers.IslandsManager; -import world.bentobox.bentobox.managers.RanksManagerBeforeClassTest; import world.bentobox.bentobox.util.Util; @RunWith(PowerMockRunner.class) -@PrepareForTest({ BentoBox.class, Util.class }) -public class IslandCacheTest extends RanksManagerBeforeClassTest { +@PrepareForTest({ Bukkit.class, BentoBox.class, Util.class, Location.class, DatabaseSetup.class, }) +public class IslandCacheTest extends AbstractCommonSetup { + private static AbstractDatabaseHandler handler; @Mock private World world; @Mock @@ -67,6 +81,21 @@ public class IslandCacheTest extends RanksManagerBeforeClassTest { // Database Database db; + @SuppressWarnings("unchecked") + @BeforeClass + public static void beforeClass() throws IllegalAccessException, InvocationTargetException, IntrospectionException { + // This has to be done beforeClass otherwise the tests will interfere with each + // other + handler = mock(AbstractDatabaseHandler.class); + // Database + PowerMockito.mockStatic(DatabaseSetup.class); + DatabaseSetup dbSetup = mock(DatabaseSetup.class); + when(DatabaseSetup.getDatabase()).thenReturn(dbSetup); + when(dbSetup.getHandler(any())).thenReturn(handler); + when(handler.saveObject(any())).thenReturn(CompletableFuture.completedFuture(true)); + + } + @SuppressWarnings("unchecked") @Before public void setUp() throws Exception { @@ -110,14 +139,27 @@ public class IslandCacheTest extends RanksManagerBeforeClassTest { // database must be mocked here db = mock(Database.class); + when(db.loadObject(anyString())).thenReturn(island); + when(db.saveObjectAsync(any())).thenReturn(CompletableFuture.completedFuture(true)); // New cache ic = new IslandCache(db); } + @Override @After - public void tearDown() { + public void tearDown() throws Exception { + super.tearDown(); Mockito.framework().clearInlineMocks(); + deleteAll(new File("database")); + deleteAll(new File("database_backup")); + } + + private void deleteAll(File file) throws IOException { + if (file.exists()) { + Files.walk(file.toPath()).sorted(Comparator.reverseOrder()).map(Path::toFile).forEach(File::delete); + } + } /** @@ -169,9 +211,16 @@ public class IslandCacheTest extends RanksManagerBeforeClassTest { /** * Test for {@link IslandCache#getIslandAt(Location)} + * @throws IntrospectionException + * @throws NoSuchMethodException + * @throws ClassNotFoundException + * @throws InvocationTargetException + * @throws IllegalAccessException + * @throws InstantiationException */ @Test - public void testGetIslandAtLocation() { + public void testGetIslandAtLocation() throws InstantiationException, IllegalAccessException, + InvocationTargetException, ClassNotFoundException, NoSuchMethodException, IntrospectionException { // Set coords to be in island space when(island.inIslandSpace(any(Integer.class), any(Integer.class))).thenReturn(true); // Set plugin @@ -256,8 +305,12 @@ public class IslandCacheTest extends RanksManagerBeforeClassTest { @Test public void testResetAllFlags() { ic.addIsland(island); + BukkitScheduler scheduler = mock(BukkitScheduler.class); + PowerMockito.mockStatic(Bukkit.class, Mockito.RETURNS_MOCKS); + when(Bukkit.getScheduler()).thenReturn(scheduler); ic.resetAllFlags(world); - verify(island).setFlagsDefaults(); + + verify(scheduler).runTaskAsynchronously(eq(plugin), any(Runnable.class)); } /**