using System; using System.Linq; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Collections.Specialized; namespace Umbraco.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 private readonly Dictionary> _schedule = new Dictionary>(StringComparer.InvariantCultureIgnoreCase); public event NotifyCollectionChangedEventHandler CollectionChanged; private void OnCollectionChanged(NotifyCollectionChangedEventArgs args) { CollectionChanged?.Invoke(this, args); } /// /// Add an existing schedule /// /// public void Add(ContentSchedule schedule) { if (!_schedule.TryGetValue(schedule.Culture, out var 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)); } /// /// Adds a new schedule for invariant content /// /// /// public void Add(DateTime? releaseDate, DateTime? expireDate) { Add(string.Empty, releaseDate, expireDate); } /// /// Adds a new schedule for a culture /// /// /// /// public void Add(string culture, DateTime? releaseDate, DateTime? expireDate) { if (culture == null) throw new ArgumentNullException(nameof(culture)); if (releaseDate.HasValue && expireDate.HasValue && releaseDate >= expireDate) throw new InvalidOperationException($"The {nameof(releaseDate)} must be less than {nameof(expireDate)}"); if (!releaseDate.HasValue && !expireDate.HasValue) return; //TODO: Do we allow passing in a release or expiry date that is before now? if (!_schedule.TryGetValue(culture, out var 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(0, culture, releaseDate.Value, ContentScheduleChange.Start); changes.Add(releaseDate.Value, entry); OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, entry)); } if (expireDate.HasValue) { var entry = new ContentSchedule(0, culture, expireDate.Value, ContentScheduleChange.End); changes.Add(expireDate.Value, entry); OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, entry)); } } /// /// Remove a scheduled change /// /// public void Remove(ContentSchedule change) { if (_schedule.TryGetValue(change.Culture, out var 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(ContentScheduleChange changeType, DateTime? changeDate = null) { Clear(string.Empty, changeType, 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, ContentScheduleChange changeType, DateTime? changeDate = null) { if (_schedule.TryGetValue(culture, out var s)) { foreach (var ofChange in s.Where(x => x.Value.Change == changeType && (changeDate.HasValue ? x.Value.Date <= changeDate.Value : true)).ToList()) { var removed = s.Remove(ofChange.Value.Date); if (removed) { OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, ofChange.Value)); if (s.Count == 0) _schedule.Remove(culture); } } } } /// /// Returns all pending schedules based on the date and type provided /// /// /// /// public IEnumerable GetPending(ContentScheduleChange changeType, DateTime date) { if (_schedule.TryGetValue(string.Empty, out var changes)) return changes.Values.Where(x => x.Date <= date); return Enumerable.Empty(); } /// /// Gets the schedule for invariant content /// /// public IEnumerable GetSchedule(ContentScheduleChange? changeType = null) { return GetSchedule(string.Empty, changeType); } /// /// Gets the schedule for a culture /// /// /// public IEnumerable GetSchedule(string culture, ContentScheduleChange? changeType = null) { if (_schedule.TryGetValue(culture, out var changes)) return changeType == null ? changes.Values : changes.Values.Where(x => x.Change == changeType.Value); return Enumerable.Empty(); } //fixme - should this just return IEnumerable since the culture is part of the ContentSchedule object already? /// /// Returns all schedules for both invariant and variant cultures /// /// public IReadOnlyDictionary> FullSchedule => _schedule.ToDictionary(x => x.Key, x => (IEnumerable)x.Value.Values); //public IEnumerable FullSchedule => _schedule.SelectMany(x => x.Value.Values); public object DeepClone() { var clone = new ContentScheduleCollection(); foreach(var cultureSched in _schedule) { var list = new SortedList(); foreach (var schedEntry in cultureSched.Value) list.Add(schedEntry.Key, (ContentSchedule)schedEntry.Value.DeepClone()); clone._schedule[cultureSched.Key] = list; } return clone; } public override bool Equals(object obj) { if (!(obj is ContentScheduleCollection c)) return false; return Equals(c); } public bool Equals(ContentScheduleCollection other) { var thisSched = this.FullSchedule; var thatSched = other.FullSchedule; var equal = false; if (thisSched.Count == thatSched.Count) { equal = true; foreach (var pair in thisSched) { if (thatSched.TryGetValue(pair.Key, out var val)) { if (val.SequenceEqual(pair.Value)) { equal = false; break; } } else { // Require key be present. equal = false; break; } } } return equal; } } }