using Microsoft.EntityFrameworkCore; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Timeline.Entities; using Timeline.Services.Exceptions; namespace Timeline.Services { [Serializable] public class InvalidBookmarkException : Exception { public InvalidBookmarkException() { } public InvalidBookmarkException(string message) : base(message) { } public InvalidBookmarkException(string message, Exception inner) : base(message, inner) { } protected InvalidBookmarkException( System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context) : base(info, context) { } } /// /// Service interface that manages timeline bookmarks. /// public interface IBookmarkTimelineService { /// /// Get bookmarks of a user. /// /// User id of bookmark owner. /// Id of Bookmark timelines in order. /// Thrown when user does not exist. Task> GetBookmarks(long userId); /// /// Check if a timeline is a bookmark. /// /// The user id. /// Timeline id. /// If true it will throw when user does not exist. /// If true it will throw when timeline does not exist. /// True if timeline is a bookmark. Otherwise false. /// Throw if user does not exist and is true. /// Thrown if timeline does not exist and is true. Task IsBookmark(long userId, long timelineId, bool checkUserExistence = true, bool checkTimelineExistence = true); /// /// Add a bookmark to tail to a user. /// /// User id of bookmark owner. /// Timeline id. /// True if timeline is added to bookmark. False if it already is. /// Thrown when user does not exist. /// Thrown when timeline does not exist. Task AddBookmark(long userId, long timelineId); /// /// Remove a bookmark from a user. /// /// User id of bookmark owner. /// Timeline id. /// True if deletion is performed. False if bookmark does not exist. /// Thrown when user does not exist. /// Thrown when timeline does not exist. Task RemoveBookmark(long userId, long timelineId); /// /// Move bookmark to a new position. /// /// User id of bookmark owner. /// Timeline name. /// New position. Starts at 1. /// Thrown when user does not exist. /// Thrown when timeline does not exist. /// Thrown when the timeline is not a bookmark. Task MoveBookmark(long userId, long timelineId, long newPosition); } public class BookmarkTimelineService : IBookmarkTimelineService { private readonly DatabaseContext _database; private readonly IBasicUserService _userService; private readonly IBasicTimelineService _timelineService; public BookmarkTimelineService(DatabaseContext database, IBasicUserService userService, IBasicTimelineService timelineService) { _database = database; _userService = userService; _timelineService = timelineService; } public async Task AddBookmark(long userId, long timelineId) { if (!await _userService.CheckUserExistence(userId)) throw new UserNotExistException(userId); if (!await _timelineService.CheckExistence(timelineId)) throw new TimelineNotExistException(timelineId); if (await _database.BookmarkTimelines.AnyAsync(t => t.TimelineId == timelineId && t.UserId == userId)) return false; _database.BookmarkTimelines.Add(new BookmarkTimelineEntity { TimelineId = timelineId, UserId = userId, Rank = (await _database.BookmarkTimelines.CountAsync(t => t.UserId == userId)) + 1 }); await _database.SaveChangesAsync(); return true; } public async Task> GetBookmarks(long userId) { if (!await _userService.CheckUserExistence(userId)) throw new UserNotExistException(userId); var entities = await _database.BookmarkTimelines.Where(t => t.UserId == userId).OrderBy(t => t.Rank).Select(t => new { t.TimelineId }).ToListAsync(); return entities.Select(e => e.TimelineId).ToList(); } public async Task IsBookmark(long userId, long timelineId, bool checkUserExistence = true, bool checkTimelineExistence = true) { if (checkUserExistence && !await _userService.CheckUserExistence(userId)) throw new UserNotExistException(userId); if (checkTimelineExistence && !await _timelineService.CheckExistence(timelineId)) throw new TimelineNotExistException(timelineId); return await _database.BookmarkTimelines.AnyAsync(b => b.TimelineId == timelineId && b.UserId == userId); } public async Task MoveBookmark(long userId, long timelineId, long newPosition) { if (!await _userService.CheckUserExistence(userId)) throw new UserNotExistException(userId); if (!await _timelineService.CheckExistence(timelineId)) throw new TimelineNotExistException(timelineId); var entity = await _database.BookmarkTimelines.SingleOrDefaultAsync(t => t.TimelineId == timelineId && t.UserId == userId); if (entity == null) throw new InvalidBookmarkException("You can't move a non-bookmark timeline."); var oldPosition = entity.Rank; if (newPosition < 1) { newPosition = 1; } else { var totalCount = await _database.BookmarkTimelines.CountAsync(t => t.UserId == userId); if (newPosition > totalCount) newPosition = totalCount; } if (oldPosition == newPosition) return; await using var transaction = await _database.Database.BeginTransactionAsync(); if (newPosition > oldPosition) { await _database.Database.ExecuteSqlRawAsync("UPDATE `bookmark_timelines` SET `rank` = `rank` - 1 WHERE `rank` BETWEEN {0} AND {1} AND `user` = {2}", oldPosition + 1, newPosition, userId); await _database.Database.ExecuteSqlRawAsync("UPDATE `bookmark_timelines` SET `rank` = {0} WHERE `id` = {1}", newPosition, entity.Id); } else { await _database.Database.ExecuteSqlRawAsync("UPDATE `bookmark_timelines` SET `rank` = `rank` + 1 WHERE `rank` BETWEEN {0} AND {1} AND `user` = {2}", newPosition, oldPosition - 1, userId); await _database.Database.ExecuteSqlRawAsync("UPDATE `bookmark_timelines` SET `rank` = {0} WHERE `id` = {1}", newPosition, entity.Id); } await transaction.CommitAsync(); } public async Task RemoveBookmark(long userId, long timelineId) { if (!await _userService.CheckUserExistence(userId)) throw new UserNotExistException(userId); if (!await _timelineService.CheckExistence(timelineId)) throw new TimelineNotExistException(timelineId); var entity = await _database.BookmarkTimelines.SingleOrDefaultAsync(t => t.UserId == userId && t.TimelineId == timelineId); if (entity == null) return false; await using var transaction = await _database.Database.BeginTransactionAsync(); var rank = entity.Rank; _database.BookmarkTimelines.Remove(entity); await _database.SaveChangesAsync(); await _database.Database.ExecuteSqlRawAsync("UPDATE `bookmark_timelines` SET `rank` = `rank` - 1 WHERE `rank` > {0}", rank); await transaction.CommitAsync(); return true; } } }