using System.Collections.Specialized; namespace Umbraco.Cms.Core.Models; public class ContentScheduleCollection : INotifyCollectionChanged, IDeepCloneable, IEquatable { // underlying storage for the collection backed by a sorted list so that the schedule is always in order of date and that duplicate dates per culture are not allowed private readonly Dictionary> _schedule = new(StringComparer.InvariantCultureIgnoreCase); public event NotifyCollectionChangedEventHandler? CollectionChanged; /// /// Returns all schedules registered /// /// public IReadOnlyList FullSchedule => _schedule.SelectMany(x => x.Value.Values).ToList(); public object DeepClone() { var clone = new ContentScheduleCollection(); foreach (KeyValuePair> cultureSched in _schedule) { var list = new SortedList(); foreach (KeyValuePair schedEntry in cultureSched.Value) { list.Add(schedEntry.Key, (ContentSchedule)schedEntry.Value.DeepClone()); } clone._schedule[cultureSched.Key] = list; } return clone; } public bool Equals(ContentScheduleCollection? other) { if (other == null) { return false; } Dictionary> thisSched = _schedule; Dictionary> thatSched = other._schedule; if (thisSched.Count != thatSched.Count) { return false; } foreach ((var culture, SortedList thisList) in thisSched) { // if culture is missing, or actions differ, false if (!thatSched.TryGetValue(culture, out SortedList? thatList) || !thatList.SequenceEqual(thisList)) { return false; } } return true; } public static ContentScheduleCollection CreateWithEntry(DateTime? release, DateTime? expire) { var schedule = new ContentScheduleCollection(); schedule.Add(string.Empty, release, expire); return schedule; } /// /// Clears all event handlers /// public void ClearCollectionChangedEvents() => CollectionChanged = null; /// /// Add an existing schedule /// /// public void Add(ContentSchedule schedule) { if (!_schedule.TryGetValue(schedule.Culture, out SortedList? changes)) { changes = new SortedList(); _schedule[schedule.Culture] = changes; } // TODO: Below will throw if there are duplicate dates added, validate/return bool? changes.Add(schedule.Date, schedule); OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, schedule)); } private void OnCollectionChanged(NotifyCollectionChangedEventArgs args) => CollectionChanged?.Invoke(this, args); /// /// Adds a new schedule for invariant content /// /// /// public bool Add(DateTime? releaseDate, DateTime? expireDate) => Add(string.Empty, releaseDate, expireDate); /// /// Adds a new schedule for a culture /// /// /// /// /// true if successfully added, false if validation fails public bool Add(string? culture, DateTime? releaseDate, DateTime? expireDate) { if (culture == null) { throw new ArgumentNullException(nameof(culture)); } if (releaseDate.HasValue && expireDate.HasValue && releaseDate >= expireDate) { return false; } if (!releaseDate.HasValue && !expireDate.HasValue) { return false; } // TODO: Do we allow passing in a release or expiry date that is before now? if (!_schedule.TryGetValue(culture, out SortedList? changes)) { changes = new SortedList(); _schedule[culture] = changes; } // TODO: Below will throw if there are duplicate dates added, should validate/return bool? // but the bool won't indicate which date was in error, maybe have 2 diff methods to schedule start/end? if (releaseDate.HasValue) { var entry = new ContentSchedule(culture, releaseDate.Value, ContentScheduleAction.Release); changes.Add(releaseDate.Value, entry); OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, entry)); } if (expireDate.HasValue) { var entry = new ContentSchedule(culture, expireDate.Value, ContentScheduleAction.Expire); changes.Add(expireDate.Value, entry); OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, entry)); } return true; } /// /// Remove a scheduled change /// /// public void Remove(ContentSchedule change) { if (_schedule.TryGetValue(change.Culture, out SortedList? s)) { var removed = s.Remove(change.Date); if (removed) { OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, change)); if (s.Count == 0) { _schedule.Remove(change.Culture); } } } } /// /// Clear all of the scheduled change type for invariant content /// /// /// If specified, will clear all entries with dates less than or equal to the value public void Clear(ContentScheduleAction action, DateTime? changeDate = null) => Clear(string.Empty, action, changeDate); /// /// Clear all of the scheduled change type for the culture /// /// /// /// If specified, will clear all entries with dates less than or equal to the value public void Clear(string? culture, ContentScheduleAction action, DateTime? date = null) { if (culture is null || !_schedule.TryGetValue(culture, out SortedList? schedules)) { return; } var removes = schedules.Where(x => x.Value.Action == action && (!date.HasValue || x.Value.Date <= date.Value)) .ToList(); foreach (KeyValuePair remove in removes) { var removed = schedules.Remove(remove.Value.Date); if (!removed) { continue; } OnCollectionChanged( new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, remove.Value)); } if (schedules.Count == 0) { _schedule.Remove(culture); } } /// /// Returns all pending schedules based on the date and type provided /// /// /// /// public IReadOnlyList GetPending(ContentScheduleAction action, DateTime date) => _schedule.Values.SelectMany(x => x.Values).Where(x => x.Date <= date).ToList(); /// /// Gets the schedule for invariant content /// /// public IEnumerable GetSchedule(ContentScheduleAction? action = null) => GetSchedule(string.Empty, action); /// /// Gets the schedule for a culture /// /// /// /// public IEnumerable GetSchedule(string? culture, ContentScheduleAction? action = null) { if (culture is not null && _schedule.TryGetValue(culture, out SortedList? changes)) { return action == null ? changes.Values : changes.Values.Where(x => x.Action == action.Value); } return Enumerable.Empty(); } public override bool Equals(object? obj) => obj is ContentScheduleCollection other && Equals(other); public static ContentScheduleCollection CreateWithEntry(string culture, DateTime? release, DateTime? expire) { var schedule = new ContentScheduleCollection(); schedule.Add(culture, release, expire); return schedule; } public override int GetHashCode() { throw new NotImplementedException(); } }