refactor: extract ChannelAdminApiController and clean up ChannelDirectoryService

- Extract admin endpoint group (add/remove/list admins, suggestions) into dedicated ChannelAdminApiController
- ChannelApiController reduced from 665 to 520 lines; removes 3 dependency injections
- ChannelDirectoryService: extract resolveOrderForSort() helper from bulk-reorder comparator lambda
- Remove TODO comment from ChannelApiController (partially addressed by this split)
This commit is contained in:
2026-04-21 16:32:00 +02:00
parent 3511936a29
commit 2e6ead0382
3 changed files with 227 additions and 163 deletions
@@ -0,0 +1,188 @@
package dev.kruhlmann.imgfloat.controller;
import dev.kruhlmann.imgfloat.model.OauthSessionUser;
import dev.kruhlmann.imgfloat.model.api.response.TwitchUserProfile;
import dev.kruhlmann.imgfloat.model.api.request.AdminRequest;
import dev.kruhlmann.imgfloat.service.AuthorizationService;
import dev.kruhlmann.imgfloat.service.ChannelDirectoryService;
import dev.kruhlmann.imgfloat.service.TwitchUserLookupService;
import dev.kruhlmann.imgfloat.util.LogSanitizer;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.Valid;
import java.util.Collection;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity;
import org.springframework.lang.Nullable;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;
import org.springframework.security.oauth2.core.AbstractOAuth2Token;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* Manages channel admin membership (add, remove, list) and Twitch moderator suggestions.
* Extracted from {@link ChannelApiController} to reduce its surface area.
*/
@RestController
@RequestMapping("/api/channels/{broadcaster}/admins")
@SecurityRequirement(name = "twitchOAuth")
public class ChannelAdminApiController {
private static final Logger LOG = LoggerFactory.getLogger(ChannelAdminApiController.class);
private final ChannelDirectoryService channelDirectoryService;
private final TwitchUserLookupService twitchUserLookupService;
private final AuthorizationService authorizationService;
private final OAuth2AuthorizedClientService authorizedClientService;
private final OAuth2AuthorizedClientRepository authorizedClientRepository;
public ChannelAdminApiController(
ChannelDirectoryService channelDirectoryService,
TwitchUserLookupService twitchUserLookupService,
AuthorizationService authorizationService,
OAuth2AuthorizedClientService authorizedClientService,
OAuth2AuthorizedClientRepository authorizedClientRepository
) {
this.channelDirectoryService = channelDirectoryService;
this.twitchUserLookupService = twitchUserLookupService;
this.authorizationService = authorizationService;
this.authorizedClientService = authorizedClientService;
this.authorizedClientRepository = authorizedClientRepository;
}
@PostMapping
public ResponseEntity<Boolean> addAdmin(
@PathVariable("broadcaster") String broadcaster,
@Valid @RequestBody AdminRequest request,
OAuth2AuthenticationToken oauthToken
) {
String sessionUsername = OauthSessionUser.from(oauthToken).login();
String logBroadcaster = LogSanitizer.sanitize(broadcaster);
String logSessionUsername = LogSanitizer.sanitize(sessionUsername);
String logRequestUsername = LogSanitizer.sanitize(request.username());
authorizationService.userMatchesSessionUsernameOrThrowHttpError(broadcaster, sessionUsername);
LOG.info("User {} adding admin {} to {}", logSessionUsername, logRequestUsername, logBroadcaster);
boolean added = channelDirectoryService.addAdmin(broadcaster, request.username(), sessionUsername);
if (!added) {
LOG.info("User {} already admin for {} or could not be added", logRequestUsername, logBroadcaster);
}
return ResponseEntity.ok(added);
}
@GetMapping
public Collection<TwitchUserProfile> listAdmins(
@PathVariable("broadcaster") String broadcaster,
OAuth2AuthenticationToken oauthToken,
HttpServletRequest request
) {
String sessionUsername = OauthSessionUser.from(oauthToken).login();
String logBroadcaster = LogSanitizer.sanitize(broadcaster);
String logSessionUsername = LogSanitizer.sanitize(sessionUsername);
authorizationService.userMatchesSessionUsernameOrThrowHttpError(broadcaster, sessionUsername);
LOG.debug("Listing admins for {} by {}", logBroadcaster, logSessionUsername);
var channel = channelDirectoryService.getOrCreateChannel(broadcaster);
List<String> admins = channel.getAdmins().stream().sorted(Comparator.naturalOrder()).toList();
OAuth2AuthorizedClient authorizedClient = resolveAuthorizedClient(oauthToken, request);
String accessToken = Optional.ofNullable(authorizedClient)
.map(OAuth2AuthorizedClient::getAccessToken)
.map(AbstractOAuth2Token::getTokenValue)
.orElse(null);
String clientId = Optional.ofNullable(authorizedClient)
.map(OAuth2AuthorizedClient::getClientRegistration)
.map(ClientRegistration::getClientId)
.orElse(null);
return twitchUserLookupService.fetchProfiles(admins, accessToken, clientId);
}
@GetMapping("/suggestions")
public Collection<TwitchUserProfile> listAdminSuggestions(
@PathVariable("broadcaster") String broadcaster,
OAuth2AuthenticationToken oauthToken,
HttpServletRequest request
) {
String sessionUsername = OauthSessionUser.from(oauthToken).login();
String logBroadcaster = LogSanitizer.sanitize(broadcaster);
String logSessionUsername = LogSanitizer.sanitize(sessionUsername);
authorizationService.userMatchesSessionUsernameOrThrowHttpError(broadcaster, sessionUsername);
LOG.debug("Listing admin suggestions for {} by {}", logBroadcaster, logSessionUsername);
var channel = channelDirectoryService.getOrCreateChannel(broadcaster);
OAuth2AuthorizedClient authorizedClient = resolveAuthorizedClient(oauthToken, request);
if (authorizedClient == null) {
LOG.warn(
"No authorized Twitch client found for {} while fetching admin suggestions for {}",
logSessionUsername,
logBroadcaster
);
return List.of();
}
String accessToken = Optional.of(authorizedClient)
.map(OAuth2AuthorizedClient::getAccessToken)
.map(AbstractOAuth2Token::getTokenValue)
.orElse(null);
String clientId = Optional.of(authorizedClient)
.map(OAuth2AuthorizedClient::getClientRegistration)
.map(ClientRegistration::getClientId)
.orElse(null);
if (accessToken == null || accessToken.isBlank() || clientId == null || clientId.isBlank()) {
LOG.warn(
"Missing Twitch credentials for {} while fetching admin suggestions for {}",
logSessionUsername,
logBroadcaster
);
return List.of();
}
return twitchUserLookupService.fetchModerators(broadcaster, channel.getAdmins(), accessToken, clientId);
}
@DeleteMapping("/{username}")
public ResponseEntity<Boolean> removeAdmin(
@PathVariable("broadcaster") String broadcaster,
@PathVariable("username") String username,
OAuth2AuthenticationToken oauthToken
) {
String sessionUsername = OauthSessionUser.from(oauthToken).login();
String logBroadcaster = LogSanitizer.sanitize(broadcaster);
String logSessionUsername = LogSanitizer.sanitize(sessionUsername);
String logUsername = LogSanitizer.sanitize(username);
authorizationService.userMatchesSessionUsernameOrThrowHttpError(broadcaster, sessionUsername);
LOG.info("User {} removing admin {} from {}", logSessionUsername, logUsername, logBroadcaster);
boolean removed = channelDirectoryService.removeAdmin(broadcaster, username, sessionUsername);
return ResponseEntity.ok(removed);
}
private OAuth2AuthorizedClient resolveAuthorizedClient(
@Nullable OAuth2AuthenticationToken oauthToken,
HttpServletRequest request
) {
if (oauthToken == null) {
LOG.error("Attempt to resolve authorized client without oauth token");
return null;
}
OAuth2AuthorizedClient sessionClient = authorizedClientRepository.loadAuthorizedClient(
oauthToken.getAuthorizedClientRegistrationId(),
oauthToken,
request
);
if (sessionClient != null) {
return sessionClient;
}
return authorizedClientService.loadAuthorizedClient(
oauthToken.getAuthorizedClientRegistrationId(),
oauthToken.getName()
);
}
}
@@ -3,187 +3,66 @@ package dev.kruhlmann.imgfloat.controller;
import static org.springframework.http.HttpStatus.BAD_REQUEST; import static org.springframework.http.HttpStatus.BAD_REQUEST;
import static org.springframework.http.HttpStatus.NOT_FOUND; import static org.springframework.http.HttpStatus.NOT_FOUND;
import dev.kruhlmann.imgfloat.model.api.request.AdminRequest; import dev.kruhlmann.imgfloat.model.api.request.AssetOrderRequest;
import dev.kruhlmann.imgfloat.model.api.response.AssetView;
import dev.kruhlmann.imgfloat.model.api.request.CanvasSettingsRequest; import dev.kruhlmann.imgfloat.model.api.request.CanvasSettingsRequest;
import dev.kruhlmann.imgfloat.model.api.request.ChannelScriptSettingsRequest; import dev.kruhlmann.imgfloat.model.api.request.ChannelScriptSettingsRequest;
import dev.kruhlmann.imgfloat.model.api.request.CodeAssetRequest; import dev.kruhlmann.imgfloat.model.api.request.CodeAssetRequest;
import dev.kruhlmann.imgfloat.model.OauthSessionUser;
import dev.kruhlmann.imgfloat.model.api.request.AssetOrderRequest;
import dev.kruhlmann.imgfloat.model.api.request.PlaybackRequest; import dev.kruhlmann.imgfloat.model.api.request.PlaybackRequest;
import dev.kruhlmann.imgfloat.model.api.response.ScriptAssetAttachmentView;
import dev.kruhlmann.imgfloat.model.api.request.TransformRequest; import dev.kruhlmann.imgfloat.model.api.request.TransformRequest;
import dev.kruhlmann.imgfloat.model.api.response.TwitchUserProfile;
import dev.kruhlmann.imgfloat.model.api.request.VisibilityRequest; import dev.kruhlmann.imgfloat.model.api.request.VisibilityRequest;
import dev.kruhlmann.imgfloat.model.api.response.AssetView;
import dev.kruhlmann.imgfloat.model.api.response.ScriptAssetAttachmentView;
import dev.kruhlmann.imgfloat.model.OauthSessionUser;
import dev.kruhlmann.imgfloat.service.AuthorizationService; import dev.kruhlmann.imgfloat.service.AuthorizationService;
import dev.kruhlmann.imgfloat.service.ChannelDirectoryService; import dev.kruhlmann.imgfloat.service.ChannelDirectoryService;
import dev.kruhlmann.imgfloat.service.ChannelSettingsService; import dev.kruhlmann.imgfloat.service.ChannelSettingsService;
import dev.kruhlmann.imgfloat.service.TwitchUserLookupService;
import dev.kruhlmann.imgfloat.util.LogSanitizer; import dev.kruhlmann.imgfloat.util.LogSanitizer;
import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import java.io.IOException; import java.io.IOException;
import java.util.Collection; import java.util.Collection;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.lang.Nullable;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;
import org.springframework.security.oauth2.core.AbstractOAuth2Token;
import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.server.ResponseStatusException; import org.springframework.web.server.ResponseStatusException;
/**
* Manages assets, canvas settings, and script attachments for a specific broadcaster channel.
* Admin management has been extracted to {@link ChannelAdminApiController}.
*/
@RestController @RestController
@RequestMapping("/api/channels/{broadcaster}") @RequestMapping("/api/channels/{broadcaster}")
@SecurityRequirement(name = "twitchOAuth") @SecurityRequirement(name = "twitchOAuth")
public class ChannelApiController { public class ChannelApiController {
// TODO: Code smell Controller surface area is very large, suggesting too many endpoint responsibilities in one type.
private static final Logger LOG = LoggerFactory.getLogger(ChannelApiController.class); private static final Logger LOG = LoggerFactory.getLogger(ChannelApiController.class);
private final ChannelDirectoryService channelDirectoryService; private final ChannelDirectoryService channelDirectoryService;
private final ChannelSettingsService channelSettingsService; private final ChannelSettingsService channelSettingsService;
private final OAuth2AuthorizedClientService authorizedClientService;
private final OAuth2AuthorizedClientRepository authorizedClientRepository;
private final TwitchUserLookupService twitchUserLookupService;
private final AuthorizationService authorizationService; private final AuthorizationService authorizationService;
public ChannelApiController( public ChannelApiController(
ChannelDirectoryService channelDirectoryService, ChannelDirectoryService channelDirectoryService,
ChannelSettingsService channelSettingsService, ChannelSettingsService channelSettingsService,
OAuth2AuthorizedClientService authorizedClientService,
OAuth2AuthorizedClientRepository authorizedClientRepository,
TwitchUserLookupService twitchUserLookupService,
AuthorizationService authorizationService AuthorizationService authorizationService
) { ) {
this.channelDirectoryService = channelDirectoryService; this.channelDirectoryService = channelDirectoryService;
this.channelSettingsService = channelSettingsService; this.channelSettingsService = channelSettingsService;
this.authorizedClientService = authorizedClientService;
this.authorizedClientRepository = authorizedClientRepository;
this.twitchUserLookupService = twitchUserLookupService;
this.authorizationService = authorizationService; this.authorizationService = authorizationService;
} }
@PostMapping("/admins")
public ResponseEntity<Boolean> addAdmin(
@PathVariable("broadcaster") String broadcaster,
@Valid @RequestBody AdminRequest request,
OAuth2AuthenticationToken oauthToken
) {
String sessionUsername = OauthSessionUser.from(oauthToken).login();
String logBroadcaster = LogSanitizer.sanitize(broadcaster);
String logSessionUsername = LogSanitizer.sanitize(sessionUsername);
String logRequestUsername = LogSanitizer.sanitize(request.username());
authorizationService.userMatchesSessionUsernameOrThrowHttpError(broadcaster, sessionUsername);
LOG.info("User {} adding admin {} to {}", logSessionUsername, logRequestUsername, logBroadcaster);
boolean added = channelDirectoryService.addAdmin(broadcaster, request.username(), sessionUsername);
if (!added) {
LOG.info("User {} already admin for {} or could not be added", logRequestUsername, logBroadcaster);
}
return ResponseEntity.ok(added);
}
@GetMapping("/admins")
public Collection<TwitchUserProfile> listAdmins(
@PathVariable("broadcaster") String broadcaster,
OAuth2AuthenticationToken oauthToken,
HttpServletRequest request
) {
String sessionUsername = OauthSessionUser.from(oauthToken).login();
String logBroadcaster = LogSanitizer.sanitize(broadcaster);
String logSessionUsername = LogSanitizer.sanitize(sessionUsername);
authorizationService.userMatchesSessionUsernameOrThrowHttpError(broadcaster, sessionUsername);
LOG.debug("Listing admins for {} by {}", logBroadcaster, logSessionUsername);
var channel = channelDirectoryService.getOrCreateChannel(broadcaster);
List<String> admins = channel.getAdmins().stream().sorted(Comparator.naturalOrder()).toList();
OAuth2AuthorizedClient authorizedClient = resolveAuthorizedClient(oauthToken, request);
String accessToken = Optional.ofNullable(authorizedClient)
.map(OAuth2AuthorizedClient::getAccessToken)
.map(AbstractOAuth2Token::getTokenValue)
.orElse(null);
String clientId = Optional.ofNullable(authorizedClient)
.map(OAuth2AuthorizedClient::getClientRegistration)
.map(ClientRegistration::getClientId)
.orElse(null);
return twitchUserLookupService.fetchProfiles(admins, accessToken, clientId);
}
@GetMapping("/admins/suggestions")
public Collection<TwitchUserProfile> listAdminSuggestions(
@PathVariable("broadcaster") String broadcaster,
OAuth2AuthenticationToken oauthToken,
HttpServletRequest request
) {
String sessionUsername = OauthSessionUser.from(oauthToken).login();
String logBroadcaster = LogSanitizer.sanitize(broadcaster);
String logSessionUsername = LogSanitizer.sanitize(sessionUsername);
authorizationService.userMatchesSessionUsernameOrThrowHttpError(broadcaster, sessionUsername);
LOG.debug("Listing admin suggestions for {} by {}", logBroadcaster, logSessionUsername);
var channel = channelDirectoryService.getOrCreateChannel(broadcaster);
OAuth2AuthorizedClient authorizedClient = resolveAuthorizedClient(oauthToken, request);
if (authorizedClient == null) {
LOG.warn(
"No authorized Twitch client found for {} while fetching admin suggestions for {}",
logSessionUsername,
logBroadcaster
);
return List.of();
}
String accessToken = Optional.of(authorizedClient)
.map(OAuth2AuthorizedClient::getAccessToken)
.map(AbstractOAuth2Token::getTokenValue)
.orElse(null);
String clientId = Optional.of(authorizedClient)
.map(OAuth2AuthorizedClient::getClientRegistration)
.map(ClientRegistration::getClientId)
.orElse(null);
if (accessToken == null || accessToken.isBlank() || clientId == null || clientId.isBlank()) {
LOG.warn(
"Missing Twitch credentials for {} while fetching admin suggestions for {}",
logSessionUsername,
logBroadcaster
);
return List.of();
}
return twitchUserLookupService.fetchModerators(broadcaster, channel.getAdmins(), accessToken, clientId);
}
@DeleteMapping("/admins/{username}")
public ResponseEntity<Boolean> removeAdmin(
@PathVariable("broadcaster") String broadcaster,
@PathVariable("username") String username,
OAuth2AuthenticationToken oauthToken
) {
String sessionUsername = OauthSessionUser.from(oauthToken).login();
String logBroadcaster = LogSanitizer.sanitize(broadcaster);
String logSessionUsername = LogSanitizer.sanitize(sessionUsername);
String logUsername = LogSanitizer.sanitize(username);
authorizationService.userMatchesSessionUsernameOrThrowHttpError(broadcaster, sessionUsername);
LOG.info("User {} removing admin {} from {}", logSessionUsername, logUsername, logBroadcaster);
boolean removed = channelDirectoryService.removeAdmin(broadcaster, username, sessionUsername);
return ResponseEntity.ok(removed);
}
@GetMapping("/assets") @GetMapping("/assets")
public Collection<AssetView> listAssets(@PathVariable("broadcaster") String broadcaster) { public Collection<AssetView> listAssets(@PathVariable("broadcaster") String broadcaster) {
return channelDirectoryService.getAssetsForAdmin(broadcaster); return channelDirectoryService.getAssetsForAdmin(broadcaster);
@@ -505,15 +384,6 @@ public class ChannelApiController {
.orElseThrow(this::createAsset404); .orElseThrow(this::createAsset404);
} }
private String contentDispositionFor(String mediaType) {
if (
dev.kruhlmann.imgfloat.service.media.MediaDetectionService.isInlineDisplayType(mediaType)
) {
return "inline";
}
return "attachment";
}
@DeleteMapping("/assets/{assetId}") @DeleteMapping("/assets/{assetId}")
public ResponseEntity<Void> delete( public ResponseEntity<Void> delete(
@PathVariable("broadcaster") String broadcaster, @PathVariable("broadcaster") String broadcaster,
@@ -637,29 +507,14 @@ public class ChannelApiController {
return ResponseEntity.ok().build(); return ResponseEntity.ok().build();
} }
private String contentDispositionFor(String mediaType) {
if (dev.kruhlmann.imgfloat.service.media.MediaDetectionService.isInlineDisplayType(mediaType)) {
return "inline";
}
return "attachment";
}
private ResponseStatusException createAsset404() { private ResponseStatusException createAsset404() {
return new ResponseStatusException(NOT_FOUND, "Asset not found"); return new ResponseStatusException(NOT_FOUND, "Asset not found");
} }
private OAuth2AuthorizedClient resolveAuthorizedClient(
@Nullable OAuth2AuthenticationToken oauthToken,
HttpServletRequest request
) {
if (oauthToken == null) {
LOG.error("Attempt to resolve authorized client without oauth token");
return null;
}
OAuth2AuthorizedClient sessionClient = authorizedClientRepository.loadAuthorizedClient(
oauthToken.getAuthorizedClientRegistrationId(),
oauthToken,
request
);
if (sessionClient != null) {
return sessionClient;
}
return authorizedClientService.loadAuthorizedClient(
oauthToken.getAuthorizedClientRegistrationId(),
oauthToken.getName()
);
}
} }
@@ -957,8 +957,8 @@ public class ChannelDirectoryService {
} }
List<Asset> ordered = new ArrayList<>(bucket); List<Asset> ordered = new ArrayList<>(bucket);
ordered.sort((a, b) -> { ordered.sort((a, b) -> {
int orderA = desiredOrder.getOrDefault(a.getId(), a.getDisplayOrder() != null ? a.getDisplayOrder() : bucket.size() - originalIndex.getOrDefault(a.getId(), bucket.size())); int orderA = resolveOrderForSort(a, desiredOrder, originalIndex, bucket.size());
int orderB = desiredOrder.getOrDefault(b.getId(), b.getDisplayOrder() != null ? b.getDisplayOrder() : bucket.size() - originalIndex.getOrDefault(b.getId(), bucket.size())); int orderB = resolveOrderForSort(b, desiredOrder, originalIndex, bucket.size());
int cmp = Integer.compare(orderB, orderA); int cmp = Integer.compare(orderB, orderA);
if (cmp != 0) { if (cmp != 0) {
return cmp; return cmp;
@@ -1561,6 +1561,27 @@ public class ChannelDirectoryService {
return value == null ? Integer.MIN_VALUE : value; return value == null ? Integer.MIN_VALUE : value;
} }
/**
* Resolves the effective sort key for an asset during a bulk reorder operation.
* Assets with an explicit desired order use that value; others fall back to their
* current display order, or their inverse original-index position as a last resort.
*/
private int resolveOrderForSort(
Asset asset,
Map<String, Integer> desiredOrder,
Map<String, Integer> originalIndex,
int bucketSize
) {
String id = asset.getId();
if (desiredOrder.containsKey(id)) {
return desiredOrder.get(id);
}
if (asset.getDisplayOrder() != null) {
return asset.getDisplayOrder();
}
return bucketSize - originalIndex.getOrDefault(id, bucketSize);
}
private List<Asset> updateDisplayOrder(String broadcaster, Asset target, int desiredOrder, Set<AssetType> types) { private List<Asset> updateDisplayOrder(String broadcaster, Asset target, int desiredOrder, Set<AssetType> types) {
if (target == null || types == null || types.isEmpty()) { if (target == null || types == null || types.isEmpty()) {
return List.of(); return List.of();