diff --git a/API/api/src/main/java/tc/oc/api/docs/virtual/MapDoc.java b/API/api/src/main/java/tc/oc/api/docs/virtual/MapDoc.java index e52634c..24983c2 100644 --- a/API/api/src/main/java/tc/oc/api/docs/virtual/MapDoc.java +++ b/API/api/src/main/java/tc/oc/api/docs/virtual/MapDoc.java @@ -5,6 +5,7 @@ import java.util.Collection; import java.util.List; import java.util.Set; import java.util.UUID; +import java.util.stream.Stream; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -50,6 +51,12 @@ public interface MapDoc extends Model { Collection author_uuids(); Collection contributor_uuids(); + @Serialize(false) + default Stream authorAndContributorUuids() { + return Stream.concat(author_uuids().stream(), + contributor_uuids().stream()); + } + @Serialize interface Team extends Model { @Nonnull String name(); diff --git a/API/api/src/main/java/tc/oc/api/maps/MapService.java b/API/api/src/main/java/tc/oc/api/maps/MapService.java index cb34e25..e76ce69 100644 --- a/API/api/src/main/java/tc/oc/api/maps/MapService.java +++ b/API/api/src/main/java/tc/oc/api/maps/MapService.java @@ -13,5 +13,8 @@ public interface MapService extends ModelService { ListenableFuture getRatings(MapRatingsRequest request); - ListenableFuture updateMapsAndLookupAuthors(Collection maps); + /** + * Send map updates to the backend, and retrieve data about map contributors. + */ + UpdateMapsResponse updateMaps(Collection maps); } diff --git a/API/api/src/main/java/tc/oc/api/maps/NullMapService.java b/API/api/src/main/java/tc/oc/api/maps/NullMapService.java index 4f383bc..a7d16d5 100644 --- a/API/api/src/main/java/tc/oc/api/maps/NullMapService.java +++ b/API/api/src/main/java/tc/oc/api/maps/NullMapService.java @@ -7,6 +7,7 @@ import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import tc.oc.api.docs.MapRating; import tc.oc.api.docs.virtual.MapDoc; +import tc.oc.api.message.types.UpdateMultiResponse; import tc.oc.api.model.NullModelService; public class NullMapService extends NullModelService implements MapService { @@ -22,7 +23,10 @@ public class NullMapService extends NullModelService implements } @Override - public ListenableFuture updateMapsAndLookupAuthors(Collection maps) { - return Futures.immediateFuture(new MapUpdateMultiResponse(Collections.emptyMap())); + public UpdateMapsResponse updateMaps(Collection maps) { + return new UpdateMapsResponse( + Futures.immediateFuture(UpdateMultiResponse.EMPTY), + Collections.emptyMap() + ); } } diff --git a/API/api/src/main/java/tc/oc/api/maps/UpdateMapsResponse.java b/API/api/src/main/java/tc/oc/api/maps/UpdateMapsResponse.java new file mode 100644 index 0000000..8dfbd75 --- /dev/null +++ b/API/api/src/main/java/tc/oc/api/maps/UpdateMapsResponse.java @@ -0,0 +1,38 @@ +package tc.oc.api.maps; + +import java.util.Collection; +import java.util.Map; +import java.util.UUID; + +import com.google.common.util.concurrent.ListenableFuture; +import tc.oc.api.docs.virtual.UserDoc; +import tc.oc.api.message.types.UpdateMultiResponse; + +/** + * Result of {@link MapService#updateMaps(Collection)} + */ +public class UpdateMapsResponse { + + private final ListenableFuture maps; + private final Map> authors; + + public UpdateMapsResponse(ListenableFuture maps, Map> authors) { + this.maps = maps; + this.authors = authors; + } + + /** + * Result of updating all the maps + */ + public ListenableFuture maps() { + return maps; + } + + /** + * Result of each individual map contributor lookup, + * which may not all arrive at the same time. + */ + public Map> authors() { + return authors; + } +} diff --git a/API/api/src/main/java/tc/oc/api/users/UserSearchRequest.java b/API/api/src/main/java/tc/oc/api/users/UserSearchRequest.java index 32433b5..a1aae41 100644 --- a/API/api/src/main/java/tc/oc/api/users/UserSearchRequest.java +++ b/API/api/src/main/java/tc/oc/api/users/UserSearchRequest.java @@ -5,6 +5,7 @@ import javax.annotation.Nullable; import tc.oc.api.annotations.Serialize; import tc.oc.api.docs.PlayerId; import tc.oc.api.docs.virtual.Document; +import tc.oc.minecraft.api.user.UserUtils; public class UserSearchRequest implements Document { @Serialize public final String username; diff --git a/API/api/src/main/java/tc/oc/api/users/UserUtils.java b/API/api/src/main/java/tc/oc/api/users/UserUtils.java deleted file mode 100644 index 5eb708a..0000000 --- a/API/api/src/main/java/tc/oc/api/users/UserUtils.java +++ /dev/null @@ -1,10 +0,0 @@ -package tc.oc.api.users; - -import tc.oc.commons.core.formatting.StringUtils; - -public interface UserUtils { - - static String sanitizeUsername(String username) { - return StringUtils.truncate(username.replaceAll("[^A-Za-z0-9_]", ""), 16); - } -} diff --git a/API/minecraft/src/main/java/tc/oc/api/minecraft/maps/LocalMapService.java b/API/minecraft/src/main/java/tc/oc/api/minecraft/maps/LocalMapService.java index fc3d032..84ea55a 100644 --- a/API/minecraft/src/main/java/tc/oc/api/minecraft/maps/LocalMapService.java +++ b/API/minecraft/src/main/java/tc/oc/api/minecraft/maps/LocalMapService.java @@ -2,7 +2,6 @@ package tc.oc.api.minecraft.maps; import java.util.Collection; import java.util.Collections; -import java.util.stream.Stream; import javax.inject.Inject; import javax.inject.Singleton; @@ -13,9 +12,12 @@ import tc.oc.api.docs.virtual.MapDoc; import tc.oc.api.maps.MapRatingsRequest; import tc.oc.api.maps.MapRatingsResponse; import tc.oc.api.maps.MapService; -import tc.oc.api.maps.MapUpdateMultiResponse; +import tc.oc.api.maps.UpdateMapsResponse; +import tc.oc.api.message.types.UpdateMultiResponse; import tc.oc.api.minecraft.users.UserStore; import tc.oc.api.model.NullModelService; +import tc.oc.api.users.UserService; +import tc.oc.api.util.UUIDs; import tc.oc.commons.core.stream.Collectors; import tc.oc.minecraft.api.entity.Player; @@ -23,6 +25,7 @@ import tc.oc.minecraft.api.entity.Player; public class LocalMapService extends NullModelService implements MapService { @Inject private UserStore userStore; + @Inject private UserService userService; @Override public ListenableFuture rate(MapRating rating) { @@ -35,14 +38,13 @@ public class LocalMapService extends NullModelService implements } @Override - public ListenableFuture updateMapsAndLookupAuthors(Collection maps) { - return Futures.immediateFuture(new MapUpdateMultiResponse( + public UpdateMapsResponse updateMaps(Collection maps) { + return new UpdateMapsResponse( + Futures.immediateFuture(UpdateMultiResponse.EMPTY), maps.stream() - .flatMap(map -> Stream.concat(map.author_uuids().stream(), - map.contributor_uuids().stream())) - .collect(Collectors.mappingTo(uuid -> userStore.byUuid(uuid) - .flatMap(userStore::user) - .orElse(null))) - )); + .flatMap(MapDoc::authorAndContributorUuids) + .distinct() + .collect(Collectors.mappingTo(uuid -> (ListenableFuture) userService.find(() -> UUIDs.normalize(uuid)))) + ); } } diff --git a/API/minecraft/src/main/java/tc/oc/api/minecraft/servers/LocalServerDocument.java b/API/minecraft/src/main/java/tc/oc/api/minecraft/servers/LocalServerDocument.java index b2bc9f4..d3b8eca 100644 --- a/API/minecraft/src/main/java/tc/oc/api/minecraft/servers/LocalServerDocument.java +++ b/API/minecraft/src/main/java/tc/oc/api/minecraft/servers/LocalServerDocument.java @@ -17,7 +17,7 @@ import tc.oc.api.docs.virtual.MapDoc; import tc.oc.api.docs.virtual.MatchDoc; import tc.oc.api.docs.virtual.ServerDoc; import tc.oc.api.minecraft.config.MinecraftApiConfiguration; -import tc.oc.minecraft.api.entity.OfflinePlayer; +import tc.oc.minecraft.api.user.User; import tc.oc.minecraft.api.server.LocalServer; @Singleton @@ -162,8 +162,8 @@ public class LocalServerDocument extends StartupServerDocument implements Server @Override public Map operators() { final ImmutableMap.Builder ops = ImmutableMap.builder(); - for(OfflinePlayer op : minecraftServer.getOperators()) { - ops.put(op.getUniqueId(), op.getLastKnownName().orElse("Player")); + for(User op : minecraftServer.getOperators()) { + ops.put(op.getUniqueId(), op.name().orElse("Player")); } return ops.build(); } diff --git a/API/minecraft/src/main/java/tc/oc/api/minecraft/users/LocalUserDocument.java b/API/minecraft/src/main/java/tc/oc/api/minecraft/users/LocalUserDocument.java index 3b33d56..89e0fda 100644 --- a/API/minecraft/src/main/java/tc/oc/api/minecraft/users/LocalUserDocument.java +++ b/API/minecraft/src/main/java/tc/oc/api/minecraft/users/LocalUserDocument.java @@ -15,18 +15,24 @@ import tc.oc.api.docs.SimplePlayerId; import tc.oc.api.docs.User; import tc.oc.api.docs.virtual.UserDoc; import tc.oc.api.minecraft.servers.DefaultPermissions; -import tc.oc.minecraft.api.entity.OfflinePlayer; public class LocalUserDocument extends SimplePlayerId implements User { - private final OfflinePlayer player; + private final UUID uuid; + private final String ip; - public LocalUserDocument(OfflinePlayer player) { - super(player.getUniqueId().toString(), - player.getUniqueId().toString(), - player.getLastKnownName().orElse("")); + LocalUserDocument(UUID uuid, String name, String ip) { + super(uuid.toString(), uuid.toString(), name); + this.uuid = uuid; + this.ip = ip; + } - this.player = player; + LocalUserDocument(tc.oc.minecraft.api.user.User user) { + this(user.getUniqueId(), + user.getName(), + user.onlinePlayer() + .map(p -> p.getAddress().getHostString()) + .orElse("")); } @Override @@ -41,7 +47,7 @@ public class LocalUserDocument extends SimplePlayerId implements User { @Override public UUID uuid() { - return player.getUniqueId(); + return uuid; } @Override @@ -76,9 +82,7 @@ public class LocalUserDocument extends SimplePlayerId implements User { @Override public String mc_last_sign_in_ip() { - return player.onlinePlayer() - .map(p -> p.getAddress().getHostString()) - .orElse(""); + return ip; } @Override diff --git a/API/minecraft/src/main/java/tc/oc/api/minecraft/users/LocalUserService.java b/API/minecraft/src/main/java/tc/oc/api/minecraft/users/LocalUserService.java index 2bdfc39..681af50 100644 --- a/API/minecraft/src/main/java/tc/oc/api/minecraft/users/LocalUserService.java +++ b/API/minecraft/src/main/java/tc/oc/api/minecraft/users/LocalUserService.java @@ -30,39 +30,43 @@ import tc.oc.api.users.UserSearchResponse; import tc.oc.api.users.UserService; import tc.oc.api.users.UserUpdateResponse; import tc.oc.commons.core.concurrent.FutureUtils; -import tc.oc.minecraft.api.entity.OfflinePlayer; -import tc.oc.minecraft.api.server.LocalServer; +import tc.oc.minecraft.api.user.UserFinder; @Singleton -public class LocalUserService extends NullModelService implements UserService { +class LocalUserService extends NullModelService implements UserService { - @Inject private LocalServer minecraftServer; @Inject private LocalSessionFactory sessionFactory; + @Inject private UserFinder userFinder; @Override public ListenableFuture find(UserId userId) { - return Futures.immediateFuture(new LocalUserDocument(minecraftServer.getOfflinePlayer(UUID.fromString(userId.player_id())))); + return FutureUtils.mapSync( + userFinder.findUserAsync(UUID.fromString(userId.player_id())), + user -> { + if(user.hasValidId() && user.name().isPresent()) { + return new LocalUserDocument(user); + } + throw new NotFound("No user with UUID " + userId.player_id()); + } + ); } @Override public ListenableFuture search(UserSearchRequest request) { - for(OfflinePlayer player : minecraftServer.getSavedPlayers()) { - if(player.getLastKnownName() - .filter(name -> name.equalsIgnoreCase(request.username)) - .isPresent()) { - return Futures.immediateFuture(new UserSearchResponse(new LocalUserDocument(player), - player.isOnline(), - false, - null, - null)); + return FutureUtils.mapSync( + userFinder.findUserAsync(request.username), + user -> { + if(user.hasValidId()) { + return new UserSearchResponse(new LocalUserDocument(user), user.isOnline(), false, null, null); + } + throw new NotFound("No user named '" + request.username + "'"); } - } - return Futures.immediateFailedFuture(new NotFound("No user named '" + request.username + "'", null)); + ); } @Override public ListenableFuture login(LoginRequest request) { - final User user = new LocalUserDocument(minecraftServer.getOfflinePlayer(request.uuid)); + final User user = new LocalUserDocument(request.uuid, request.username, request.ip.getHostAddress()); final Session session = request.start_session ? sessionFactory.newSession(user, request.ip) : null; diff --git a/API/api/src/main/java/tc/oc/api/maps/MapUpdateMultiResponse.java b/API/ocn/src/main/java/tc/oc/api/ocn/MapUpdateMultiResponse.java similarity index 96% rename from API/api/src/main/java/tc/oc/api/maps/MapUpdateMultiResponse.java rename to API/ocn/src/main/java/tc/oc/api/ocn/MapUpdateMultiResponse.java index 19d660b..1ed79c2 100644 --- a/API/api/src/main/java/tc/oc/api/maps/MapUpdateMultiResponse.java +++ b/API/ocn/src/main/java/tc/oc/api/ocn/MapUpdateMultiResponse.java @@ -1,4 +1,4 @@ -package tc.oc.api.maps; +package tc.oc.api.ocn; import java.util.Collections; import java.util.Map; diff --git a/API/ocn/src/main/java/tc/oc/api/ocn/OCNMapService.java b/API/ocn/src/main/java/tc/oc/api/ocn/OCNMapService.java index ee4c9a4..64aff46 100644 --- a/API/ocn/src/main/java/tc/oc/api/ocn/OCNMapService.java +++ b/API/ocn/src/main/java/tc/oc/api/ocn/OCNMapService.java @@ -6,12 +6,16 @@ import javax.inject.Singleton; import com.google.common.util.concurrent.ListenableFuture; import tc.oc.api.docs.MapRating; import tc.oc.api.docs.virtual.MapDoc; +import tc.oc.api.docs.virtual.UserDoc; +import tc.oc.api.exceptions.NotFound; import tc.oc.api.http.HttpOption; import tc.oc.api.maps.MapRatingsRequest; import tc.oc.api.maps.MapRatingsResponse; import tc.oc.api.maps.MapService; -import tc.oc.api.maps.MapUpdateMultiResponse; +import tc.oc.api.maps.UpdateMapsResponse; import tc.oc.api.model.HttpModelService; +import tc.oc.commons.core.concurrent.FutureUtils; +import tc.oc.commons.core.stream.Collectors; @Singleton class OCNMapService extends HttpModelService implements MapService { @@ -24,7 +28,21 @@ class OCNMapService extends HttpModelService implements MapServi return this.client().post(memberUri(request.map_id, "get_ratings"), request, MapRatingsResponse.class, HttpOption.INFINITE_RETRY); } - public ListenableFuture updateMapsAndLookupAuthors(Collection maps) { - return updateMulti(maps, MapUpdateMultiResponse.class); + public UpdateMapsResponse updateMaps(Collection maps) { + final ListenableFuture future = updateMulti(maps, MapUpdateMultiResponse.class); + return new UpdateMapsResponse( + (ListenableFuture) future, + maps.stream() + .flatMap(MapDoc::authorAndContributorUuids) + .distinct() + .collect(Collectors.mappingTo(uuid -> FutureUtils.mapSync( + future, + response -> { + final UserDoc.Identity user = response.users_by_uuid.get(uuid); + if(user != null) return user; + throw new NotFound(); + } + ))) + ); } } diff --git a/PGM/src/main/java/tc/oc/pgm/map/MapDocument.java b/PGM/src/main/java/tc/oc/pgm/map/MapDocument.java index 9fb0e43..835a148 100644 --- a/PGM/src/main/java/tc/oc/pgm/map/MapDocument.java +++ b/PGM/src/main/java/tc/oc/pgm/map/MapDocument.java @@ -4,6 +4,7 @@ import java.net.URL; import java.nio.file.Path; import java.util.Collection; import java.util.List; +import java.util.Objects; import java.util.Set; import java.util.UUID; import javax.annotation.Nullable; @@ -25,6 +26,7 @@ public class MapDocument extends AbstractModel implements MapDoc { private final InfoModule infoModule; private final MapInfo info; private final List teams; + private final List authors, contributors; @Inject private MapDocument(MapFolder folder, InfoModule infoModule, MapInfo info, List teams) { this.folder = folder; @@ -33,6 +35,16 @@ public class MapDocument extends AbstractModel implements MapDoc { this.teams = teams.stream() .map(TeamFactory::getDocument) .collect(toImmutableList()); + + this.authors = info.authors.stream() + .map(Contributor::getUuid) + .filter(Objects::nonNull) + .collect(toImmutableList()); + + this.contributors = info.contributors.stream() + .map(Contributor::getUuid) + .filter(Objects::nonNull) + .collect(toImmutableList()); } @Override @@ -113,18 +125,12 @@ public class MapDocument extends AbstractModel implements MapDoc { @Override public Collection author_uuids() { - return info.authors.stream() - .map(Contributor::getUuid) - .filter(uuid -> uuid != null) - .collect(toImmutableList()); + return authors; } @Override public Collection contributor_uuids() { - return info.contributors.stream() - .map(Contributor::getUuid) - .filter(uuid -> uuid != null) - .collect(toImmutableList()); + return contributors; } } diff --git a/PGM/src/main/java/tc/oc/pgm/map/MapInfo.java b/PGM/src/main/java/tc/oc/pgm/map/MapInfo.java index b421640..aee3629 100644 --- a/PGM/src/main/java/tc/oc/pgm/map/MapInfo.java +++ b/PGM/src/main/java/tc/oc/pgm/map/MapInfo.java @@ -2,6 +2,7 @@ package tc.oc.pgm.map; import java.util.List; import java.util.Set; +import java.util.stream.Stream; import javax.annotation.Nullable; import com.google.common.collect.ComparisonChain; @@ -153,8 +154,8 @@ public class MapInfo implements Comparable { return Contributor.filterNamed(this.contributors); } - public Iterable getAllContributors() { - return Iterables.concat(authors, contributors); + public Stream allContributors() { + return Stream.concat(authors.stream(), contributors.stream()); } public boolean isAuthor(PlayerId player) { diff --git a/PGM/src/main/java/tc/oc/pgm/map/MapLibrary.java b/PGM/src/main/java/tc/oc/pgm/map/MapLibrary.java index 8aba744..c7fa6b8 100644 --- a/PGM/src/main/java/tc/oc/pgm/map/MapLibrary.java +++ b/PGM/src/main/java/tc/oc/pgm/map/MapLibrary.java @@ -10,7 +10,7 @@ import java.util.logging.Logger; import javax.annotation.Nullable; import com.google.common.util.concurrent.ListenableFuture; -import tc.oc.api.maps.MapUpdateMultiResponse; +import tc.oc.api.message.types.UpdateMultiResponse; public interface MapLibrary { @@ -44,7 +44,7 @@ public interface MapLibrary { Collection getDirtyMaps(); - ListenableFuture pushAllMaps(); + ListenableFuture pushAllMaps(); - ListenableFuture pushDirtyMaps(); + ListenableFuture pushDirtyMaps(); } diff --git a/PGM/src/main/java/tc/oc/pgm/map/MapLibraryImpl.java b/PGM/src/main/java/tc/oc/pgm/map/MapLibraryImpl.java index 3918e86..c59c693 100644 --- a/PGM/src/main/java/tc/oc/pgm/map/MapLibraryImpl.java +++ b/PGM/src/main/java/tc/oc/pgm/map/MapLibraryImpl.java @@ -6,6 +6,7 @@ import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.UUID; @@ -23,12 +24,14 @@ import com.google.common.collect.SetMultimap; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.SettableFuture; import tc.oc.api.docs.virtual.UserDoc; +import tc.oc.api.exceptions.NotFound; import tc.oc.api.maps.MapService; -import tc.oc.api.maps.MapUpdateMultiResponse; -import tc.oc.minecraft.scheduler.SyncExecutor; +import tc.oc.api.maps.UpdateMapsResponse; +import tc.oc.api.message.types.UpdateMultiResponse; import tc.oc.commons.core.logging.Loggers; -import tc.oc.commons.core.util.Pair; +import tc.oc.commons.core.stream.Collectors; import tc.oc.commons.core.util.SystemFutureCallback; +import tc.oc.minecraft.scheduler.SyncExecutor; @Singleton public class MapLibraryImpl implements MapLibrary { @@ -173,66 +176,73 @@ public class MapLibraryImpl implements MapLibrary { } @Override - public ListenableFuture pushAllMaps() { + public ListenableFuture pushAllMaps() { return pushMaps(getMaps()); } @Override - public ListenableFuture pushDirtyMaps() { + public ListenableFuture pushDirtyMaps() { return pushMaps(getDirtyMaps()); } - private ListenableFuture pushMaps(final Collection maps) { + private ListenableFuture pushMaps(final Collection maps) { final Set pushedMaps = ImmutableSet.copyOf(maps); - logger.info("Pushing " + pushedMaps.size() + " maps"); - final SettableFuture future = SettableFuture.create(); + final SetMultimap mapsByContributorId = maps.stream().collect(Collectors.indexingByMulti( + map -> map.getInfo() + .allContributors() + .map(Contributor::getUuid) + .filter(Objects::nonNull) + )); + + logger.info("Pushing " + pushedMaps.size() + + " maps and resolving " + mapsByContributorId.keySet().size() + " contributor names"); + + final UpdateMapsResponse response = mapService.updateMaps( + Collections2.transform(pushedMaps, PGMMap::getDocument) + ); + + response.authors().forEach( + (uuid, future) -> syncExecutor.callback( + future, + SystemFutureCallback.onSuccess( + user -> { + logger.fine(() -> "Resolved map author " + uuid + " to " + user.username()); + mapsByContributorId.removeAll(uuid).forEach( + map -> map.getInfo().allContributors().forEach(contributor -> { + if(uuid.equals(contributor.getUuid())) { + contributor.setUser(user); + } + }) + ); + } + ).onFailure(NotFound.class, ex -> + mapsByContributorId.removeAll(uuid).forEach( + map -> map.getLogger().severe("Contributor UUID not found: " + uuid) + ) + ).onCompletion(() -> { + if(mapsByContributorId.isEmpty()) { + logger.info("Finished resolving map contributors"); + } + }) + ) + ); + + final SettableFuture future = SettableFuture.create(); syncExecutor.callback( - mapService.updateMapsAndLookupAuthors(Collections2.transform(pushedMaps, PGMMap::getDocument)), - new SystemFutureCallback() { - @Override public void onSuccessThrows(MapUpdateMultiResponse result) throws Exception { - logger.info("Push complete: " + result); - logErrors(result); - pushedMaps.forEach(PGMMap::markPushed); - resolveContributors(pushedMaps, result.users_by_uuid); - future.set(result); - } - - @Override - public void onFailure(Throwable e) { - super.onFailure(e); - future.setException(e); - } - } + response.maps(), + SystemFutureCallback.onSuccess(mapResponse -> { + logger.info("Push complete: " + mapResponse); + logErrors(mapResponse); + pushedMaps.forEach(PGMMap::markPushed); + future.set(mapResponse); + }).onFailure(Throwable.class, future::setException) ); return future; } - private void resolveContributors(Collection maps, Map usersByUuid) { - Set> missing = new HashSet<>(); - - for(PGMMap map : maps) { - for(Contributor contributor : map.getInfo().getAllContributors()) { - if(contributor.needsLookup()) { - final UserDoc.Identity user = usersByUuid.get(contributor.getUuid()); - if(user != null) { - contributor.setUser(user); - } else { - missing.add(Pair.create(map, contributor)); - } - } - } - } - - logger.info("Resolved " + usersByUuid.size() + " contributor UUIDs"); - - for(Pair pair : missing) { - pair.first.getLogger().severe("Contributor UUID not found: " + pair.second.getUuid()); - } - } - - private void logErrors(MapUpdateMultiResponse response) { + private void logErrors(UpdateMultiResponse response) { for(Map.Entry>> mapEntry : response.errors.entrySet()) { final PGMMap map = needMapById(mapEntry.getKey()); for(Map.Entry> propEntry : mapEntry.getValue().entrySet()) { diff --git a/Util/bukkit/src/main/java/tc/oc/commons/bukkit/event/EventHandlerScanner.java b/Util/bukkit/src/main/java/tc/oc/commons/bukkit/event/EventHandlerScanner.java index fd59b9f..eddc951 100644 --- a/Util/bukkit/src/main/java/tc/oc/commons/bukkit/event/EventHandlerScanner.java +++ b/Util/bukkit/src/main/java/tc/oc/commons/bukkit/event/EventHandlerScanner.java @@ -68,6 +68,6 @@ public abstract class EventHandlerScanner, Info> findEventHandlers(Class listener) { return findEventHandlerMethods(listener) .map(method -> createHandlerInfo(method, findEventType(method), findAnnotation(method))) - .collect(Collectors.toSetMultimap(EventSubscriber::key)); + .collect(Collectors.toImmutableSetMultimap(EventSubscriber::key)); } } diff --git a/Util/core/src/main/java/tc/oc/commons/core/commands/CommandFutureCallback.java b/Util/core/src/main/java/tc/oc/commons/core/commands/CommandFutureCallback.java index a9b1782..4e44904 100644 --- a/Util/core/src/main/java/tc/oc/commons/core/commands/CommandFutureCallback.java +++ b/Util/core/src/main/java/tc/oc/commons/core/commands/CommandFutureCallback.java @@ -80,7 +80,7 @@ public class CommandFutureCallback extends SystemFutureCallback { } @Override - protected void onFailureUnhandled(Throwable e) { + protected void handleDefaultFailure(Throwable e) { exceptionHandlerFactory.create(audience, command) .handleException(e, this, creationSite); } diff --git a/Util/core/src/main/java/tc/oc/commons/core/concurrent/FutureUtils.java b/Util/core/src/main/java/tc/oc/commons/core/concurrent/FutureUtils.java index 3c30010..2efb75f 100644 --- a/Util/core/src/main/java/tc/oc/commons/core/concurrent/FutureUtils.java +++ b/Util/core/src/main/java/tc/oc/commons/core/concurrent/FutureUtils.java @@ -15,6 +15,7 @@ import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.Uninterruptibles; import tc.oc.commons.core.util.Pair; +import tc.oc.commons.core.util.ThrowingFunction; public final class FutureUtils { private FutureUtils() {} @@ -85,15 +86,24 @@ public final class FutureUtils { /** * Equivalent to {@link Futures#transform}, but can be passed a lambda unambiguously */ - public static ListenableFuture mapSync(ListenableFuture in, Function op) { - return Futures.transform(in, op::apply); + public static ListenableFuture mapSync(ListenableFuture in, ThrowingFunction op) { + return mapSync(in, op, MoreExecutors.sameThreadExecutor()); } - public static ListenableFuture mapSync(ListenableFuture in, Function op, Executor executor) { - return Futures.transform(in, op::apply, executor); + + public static ListenableFuture mapSync(ListenableFuture in, ThrowingFunction op, Executor executor) { + return mapAsync(in, out -> { + try { + return Futures.immediateFuture(op.applyThrows(out)); + } catch(Throwable ex) { + return Futures.immediateFailedFuture(ex); + } + }, executor); } + public static ListenableFuture mapAsync(ListenableFuture in, AsyncFunction op) { return Futures.transform(in, op); } + public static ListenableFuture mapAsync(ListenableFuture in, AsyncFunction op, Executor executor) { return Futures.transform(in, op, executor); } diff --git a/Util/core/src/main/java/tc/oc/commons/core/stream/Collectors.java b/Util/core/src/main/java/tc/oc/commons/core/stream/Collectors.java index ec64b7c..7cc5edb 100644 --- a/Util/core/src/main/java/tc/oc/commons/core/stream/Collectors.java +++ b/Util/core/src/main/java/tc/oc/commons/core/stream/Collectors.java @@ -1,6 +1,7 @@ package tc.oc.commons.core.stream; import java.util.ArrayList; +import java.util.Collections; import java.util.EnumSet; import java.util.List; import java.util.Map; @@ -11,14 +12,17 @@ import java.util.function.BinaryOperator; import java.util.function.Function; import java.util.function.Supplier; import java.util.stream.Collector; +import java.util.stream.Stream; +import com.google.common.collect.HashMultimap; import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableListMultimap; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMultimap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSetMultimap; import com.google.common.collect.Lists; +import com.google.common.collect.Multimap; +import com.google.common.collect.SetMultimap; import tc.oc.commons.core.random.Entropy; import tc.oc.commons.core.random.MutableEntropy; import tc.oc.commons.core.util.AmbiguousElementException; @@ -99,25 +103,21 @@ public final class Collectors { }; } - public static Collector> toListMultimap(Function keyMapper, Function valueMapper) { - return new MultimapCollector<>(ImmutableListMultimap::builder, keyMapper, valueMapper); + public static Collector> toImmutableSetMultimap(Function keyMapper) { + return toImmutableSetMultimap(keyMapper, identity()); } - public static Collector> toSetMultimap(Function keyMapper) { - return toSetMultimap(keyMapper, identity()); + public static Collector> toImmutableSetMultimap(Function keyMapper, Function valueMapper) { + return new ImmutableMultimapCollector<>(ImmutableSetMultimap::builder, t -> Stream.of(keyMapper.apply(t)), t -> Stream.of(valueMapper.apply(t))); } - public static Collector> toSetMultimap(Function keyMapper, Function valueMapper) { - return new MultimapCollector<>(ImmutableSetMultimap::builder, keyMapper, valueMapper); - } - - private static class MultimapCollector, R extends ImmutableMultimap> implements Collector { + private static class ImmutableMultimapCollector, R extends ImmutableMultimap> implements Collector { private final Supplier builderSupplier; - private final Function keyMapper; - private final Function valueMapper; + private final Function> keyMapper; + private final Function> valueMapper; - private MultimapCollector(Supplier builderSupplier, Function keyMapper, Function valueMapper) { + private ImmutableMultimapCollector(Supplier builderSupplier, Function> keyMapper, Function> valueMapper) { this.builderSupplier = builderSupplier; this.keyMapper = keyMapper; this.valueMapper = valueMapper; @@ -131,11 +131,11 @@ public final class Collectors { @Override public BiConsumer accumulator() { return (builder, t) -> { - final K key = keyMapper.apply(t); - final V value = valueMapper.apply(t); - if(key != null && value != null) { - builder.put(key, value); - } + keyMapper.apply(t).forEach( + key -> valueMapper.apply(t).forEach( + value -> builder.put(key, value) + ) + ); }; } @@ -160,6 +160,61 @@ public final class Collectors { } } + public static Collector> indexingByMulti(Function> keyMapper) { + return new MultimapCollector<>(HashMultimap::create, keyMapper, Stream::of); + } + + public static Collector> mappingToMulti(Function> valueMapper) { + return new MultimapCollector<>(HashMultimap::create, Stream::of, valueMapper); + } + + private static class MultimapCollector implements Collector { + + private final Supplier multimapSupplier; + private final Function> keyMapper; + private final Function> valueMapper; + + private MultimapCollector(Supplier multimapSupplier, Function> keyMapper, Function> valueMapper) { + this.multimapSupplier = multimapSupplier; + this.keyMapper = keyMapper; + this.valueMapper = valueMapper; + } + + @Override + public Supplier supplier() { + return multimapSupplier; + } + + @Override + public BiConsumer accumulator() { + return (multimap, t) -> { + keyMapper.apply(t).forEach( + key -> valueMapper.apply(t).forEach( + value -> multimap.put(key, value) + ) + ); + }; + } + + @Override + public BinaryOperator combiner() { + return (m1, m2) -> { + m1.putAll(m2); + return m1; + }; + } + + @Override + public Function finisher() { + return Function.identity(); + } + + @Override + public Set characteristics() { + return Collections.singleton(Characteristics.IDENTITY_FINISH); + } + } + private static class ListCollector implements Collector, R> { private final Function, R> finisher; diff --git a/Util/core/src/main/java/tc/oc/commons/core/util/SystemFutureCallback.java b/Util/core/src/main/java/tc/oc/commons/core/util/SystemFutureCallback.java index bccc502..91ab3c4 100644 --- a/Util/core/src/main/java/tc/oc/commons/core/util/SystemFutureCallback.java +++ b/Util/core/src/main/java/tc/oc/commons/core/util/SystemFutureCallback.java @@ -1,5 +1,7 @@ package tc.oc.commons.core.util; +import java.util.ArrayList; +import java.util.List; import java.util.Map; import javax.annotation.Nullable; import javax.inject.Inject; @@ -25,6 +27,7 @@ public class SystemFutureCallback implements FutureCallback { protected final StackTrace creationSite; private final @Nullable ThrowingConsumer successHandler; private final ListMultimap, ThrowingConsumer> failureHandlers = ArrayListMultimap.create(); + private final List> completionHandlers = new ArrayList<>(); public static SystemFutureCallback onSuccess(ThrowingConsumer handler) { return new SystemFutureCallback<>(checkNotNull(handler)); @@ -53,6 +56,11 @@ public class SystemFutureCallback implements FutureCallback { return this; } + public SystemFutureCallback onCompletion(ThrowingRunnable handler) { + completionHandlers.add(handler); + return this; + } + /** * @deprecated use {@link #SystemFutureCallback(ThrowingConsumer)} */ @@ -68,16 +76,22 @@ public class SystemFutureCallback implements FutureCallback { onSuccessThrows(result); } } catch(Throwable e) { - onFailure(e); + handleFailure(e); } - } - - protected void onFailureUnhandled(Throwable e) { - exceptionHandler.handleException(e, this, creationSite); + handleCompletion(); } @Override public void onFailure(Throwable e) { + handleFailure(e); + handleCompletion(); + } + + protected void handleDefaultFailure(Throwable e) { + exceptionHandler.handleException(e, this, creationSite); + } + + private void handleFailure(Throwable e) { boolean handled = false; for(Map.Entry, ThrowingConsumer> handler : failureHandlers.entries()) { if(handler.getKey().isInstance(e)) { @@ -85,12 +99,22 @@ public class SystemFutureCallback implements FutureCallback { handler.getValue().acceptThrows(e); handled = true; } catch(Throwable e1) { - onFailureUnhandled(e1); + handleDefaultFailure(e1); } } } if(!handled) { - onFailureUnhandled(e); + handleDefaultFailure(e); + } + } + + private void handleCompletion() { + for(ThrowingRunnable handler : completionHandlers) { + try { + handler.runThrows(); + } catch(Throwable e) { + handleFailure(e); + } } } }