diff --git a/Ical.Net.Benchmarks/Ical.Net.Benchmarks.csproj b/Ical.Net.Benchmarks/Ical.Net.Benchmarks.csproj index 18d709ea..1659d678 100644 --- a/Ical.Net.Benchmarks/Ical.Net.Benchmarks.csproj +++ b/Ical.Net.Benchmarks/Ical.Net.Benchmarks.csproj @@ -1,17 +1,13 @@  - - - net8.0;net6.0;netcoreapp3.1;net48 - Exe - latest - - - - - - - - - - + + net8.0;net6.0;netcoreapp3.1;net48 + Exe + latest + + + + + + + diff --git a/Ical.Net.Tests/CopyComponentTests.cs b/Ical.Net.Tests/CopyComponentTests.cs new file mode 100644 index 00000000..0fbeb512 --- /dev/null +++ b/Ical.Net.Tests/CopyComponentTests.cs @@ -0,0 +1,198 @@ +using Ical.Net.CalendarComponents; +using Ical.Net.DataTypes; +using Ical.Net.Serialization; +using NUnit.Framework; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Text.RegularExpressions; + +namespace Ical.Net.Tests +{ + /// + /// Tests for deep copying of ICal components. + /// + [TestFixture] + public class CopyComponentTests + { + [Test, TestCaseSource(nameof(CopyCalendarTest_TestCases)), Category("Copy tests")] + public void CopyCalendarTest(string calendarString) + { + var iCal1 = Calendar.Load(calendarString); + var iCal2 = iCal1.Copy(); + SerializationTests.CompareCalendars(iCal1, iCal2); + } + + public static IEnumerable CopyCalendarTest_TestCases() + { + yield return new TestCaseData(IcsFiles.Attachment3).SetName("Attachment3"); + yield return new TestCaseData(IcsFiles.Bug2148092).SetName("Bug2148092"); + yield return new TestCaseData(IcsFiles.CaseInsensitive1).SetName("CaseInsensitive1"); + yield return new TestCaseData(IcsFiles.CaseInsensitive2).SetName("CaseInsensitive2"); + yield return new TestCaseData(IcsFiles.CaseInsensitive3).SetName("CaseInsensitive3"); + yield return new TestCaseData(IcsFiles.Categories1).SetName("Categories1"); + yield return new TestCaseData(IcsFiles.Duration1).SetName("Duration1"); + yield return new TestCaseData(IcsFiles.Encoding1).SetName("Encoding1"); + yield return new TestCaseData(IcsFiles.Event1).SetName("Event1"); + yield return new TestCaseData(IcsFiles.Event2).SetName("Event2"); + yield return new TestCaseData(IcsFiles.Event3).SetName("Event3"); + yield return new TestCaseData(IcsFiles.Event4).SetName("Event4"); + yield return new TestCaseData(IcsFiles.GeographicLocation1).SetName("GeographicLocation1"); + yield return new TestCaseData(IcsFiles.Language1).SetName("Language1"); + yield return new TestCaseData(IcsFiles.Language2).SetName("Language2"); + yield return new TestCaseData(IcsFiles.Language3).SetName("Language3"); + yield return new TestCaseData(IcsFiles.TimeZone1).SetName("TimeZone1"); + yield return new TestCaseData(IcsFiles.TimeZone2).SetName("TimeZone2"); + yield return new TestCaseData(IcsFiles.TimeZone3).SetName("TimeZone3"); + yield return new TestCaseData(IcsFiles.XProperty1).SetName("XProperty1"); + yield return new TestCaseData(IcsFiles.XProperty2).SetName("XProperty2"); + } + + private static readonly DateTime _now = DateTime.Now; + private static readonly DateTime _later = _now.AddHours(1); + + private static CalendarEvent GetSimpleEvent() => new CalendarEvent + { + DtStart = new CalDateTime(_now), + DtEnd = new CalDateTime(_later), + Duration = TimeSpan.FromHours(1), + }; + + private static string SerializeEvent(CalendarEvent e) => new CalendarSerializer().SerializeToString(new Calendar { Events = { e } }); + + [Test] + public void CopyCalendarEventTest() + { + var orig = GetSimpleEvent(); + orig.Uid = "Hello"; + orig.Summary = "Original summary"; + orig.Resources = new[] { "A", "B" }; + orig.GeographicLocation = new GeographicLocation(48.210033, 16.363449); + orig.Transparency = TransparencyType.Opaque; + orig.Attachments.Add(new Attachment("https://original.org/")); + var copy = orig.Copy(); + + copy.Uid = "Goodbye"; + copy.Summary = "Copy summary"; + + var resourcesCopyFromOrig = new List(copy.Resources); + copy.Resources = new[] { "C", "D" }; + copy.Attachments[0].Uri = new Uri("https://copy.org/"); + const string uidPattern = "UID:"; + var serializedOrig = SerializeEvent(orig); + var serializedCopy = SerializeEvent(copy); + + Assert.Multiple(() => + { + // Should be a deep copy and changes only apply to the copy instance + Assert.That(copy.Uid, Is.Not.EqualTo(orig.Uid)); + Assert.That(copy.Summary, Is.Not.EqualTo(orig.Summary)); + Assert.That(copy.Attachments[0].Uri, Is.Not.EqualTo(orig.Attachments[0].Uri)); + Assert.That(copy.Resources[0], Is.Not.EqualTo(orig.Resources[0])); + + Assert.That(resourcesCopyFromOrig, Is.EquivalentTo(orig.Resources)); + Assert.That(copy.GeographicLocation, Is.EqualTo(orig.GeographicLocation)); + Assert.That(copy.Transparency, Is.EqualTo(orig.Transparency)); + + Assert.That(Regex.Matches(serializedOrig, uidPattern, RegexOptions.Compiled, TimeSpan.FromSeconds(100)), Has.Count.EqualTo(1)); + Assert.That(Regex.Matches(serializedCopy, uidPattern, RegexOptions.Compiled, TimeSpan.FromSeconds(100)), Has.Count.EqualTo(1)); + }); + } + + [Test] + public void CopyFreeBusyTest() + { + var orig = new FreeBusy + { + Start = new CalDateTime(_now), + End = new CalDateTime(_later), + Entries = { new FreeBusyEntry { Language = "English", StartTime = new CalDateTime(2024, 10, 1), Duration = TimeSpan.FromDays(1), Status = FreeBusyStatus.Busy}} + }; + + var copy = orig.Copy(); + + Assert.Multiple(() => + { + // Start/DtStart and End/DtEnd are the same + Assert.That(copy.Start, Is.EqualTo(orig.DtStart)); + Assert.That(copy.End, Is.EqualTo(orig.DtEnd)); + Assert.That(copy.Entries[0].Language, Is.EqualTo(orig.Entries[0].Language)); + Assert.That(copy.Entries[0].StartTime, Is.EqualTo(orig.Entries[0].StartTime)); + Assert.That(copy.Entries[0].Duration, Is.EqualTo(orig.Entries[0].Duration)); + Assert.That(copy.Entries[0].Status, Is.EqualTo(orig.Entries[0].Status)); + }); + } + + [Test] + public void CopyAlarmTest() + { + var orig = new Alarm + { + Action = AlarmAction.Display, + Trigger = new Trigger(TimeSpan.FromMinutes(15)), + Description = "Test Alarm" + }; + + var copy = orig.Copy(); + + Assert.Multiple(() => + { + Assert.That(copy.Action, Is.EqualTo(orig.Action)); + Assert.That(copy.Trigger, Is.EqualTo(orig.Trigger)); + Assert.That(copy.Description, Is.EqualTo(orig.Description)); + }); + } + + [Test] + public void CopyTodoTest() + { + var orig = new Todo + { + Summary = "Test Todo", + Description = "This is a test todo", + Due = new CalDateTime(DateTime.Now.AddDays(10)), + Priority = 1, + Contacts = new[] { "John", "Paul" }, + Status = "NeedsAction" + }; + + var copy = orig.Copy(); + + Assert.Multiple(() => + { + Assert.That(copy.Summary, Is.EqualTo(orig.Summary)); + Assert.That(copy.Description, Is.EqualTo(orig.Description)); + Assert.That(copy.Due, Is.EqualTo(orig.Due)); + Assert.That(copy.Priority, Is.EqualTo(orig.Priority)); + Assert.That(copy.Contacts, Is.EquivalentTo(orig.Contacts)); + Assert.That(copy.Status, Is.EqualTo(orig.Status)); + }); + } + + [Test] + public void CopyJournalTest() + { + var orig = new Journal + { + Summary = "Test Journal", + Description = "This is a test journal", + DtStart = new CalDateTime(DateTime.Now), + Categories = new List { "Category1", "Category2" }, + Priority = 1, + Status = "Draft" + }; + + var copy = orig.Copy(); + + Assert.Multiple(() => + { + Assert.That(copy.Summary, Is.EqualTo(orig.Summary)); + Assert.That(copy.Description, Is.EqualTo(orig.Description)); + Assert.That(copy.DtStart, Is.EqualTo(orig.DtStart)); + Assert.That(copy.Categories, Is.EquivalentTo(orig.Categories)); + Assert.That(copy.Priority, Is.EqualTo(orig.Priority)); + Assert.That(copy.Status, Is.EqualTo(orig.Status)); + }); + } + } +} diff --git a/Ical.Net.Tests/CopyTest.cs b/Ical.Net.Tests/CopyTest.cs deleted file mode 100644 index 0c9b2cdf..00000000 --- a/Ical.Net.Tests/CopyTest.cs +++ /dev/null @@ -1,77 +0,0 @@ -using Ical.Net.CalendarComponents; -using Ical.Net.DataTypes; -using Ical.Net.Serialization; -using NUnit.Framework; -using System; -using System.Collections; -using System.Text.RegularExpressions; - -namespace Ical.Net.Tests -{ - [TestFixture] - public class CopyTest - { - [Test, TestCaseSource(nameof(CopyCalendarTest_TestCases)), Category("Copy tests")] - public void CopyCalendarTest(string calendarString) - { - var iCal1 = Calendar.Load(calendarString); - var iCal2 = iCal1.Copy(); - SerializationTests.CompareCalendars(iCal1, iCal2); - } - - public static IEnumerable CopyCalendarTest_TestCases() - { - yield return new TestCaseData(IcsFiles.Attachment3).SetName("Attachment3"); - yield return new TestCaseData(IcsFiles.Bug2148092).SetName("Bug2148092"); - yield return new TestCaseData(IcsFiles.CaseInsensitive1).SetName("CaseInsensitive1"); - yield return new TestCaseData(IcsFiles.CaseInsensitive2).SetName("CaseInsensitive2"); - yield return new TestCaseData(IcsFiles.CaseInsensitive3).SetName("CaseInsensitive3"); - yield return new TestCaseData(IcsFiles.Categories1).SetName("Categories1"); - yield return new TestCaseData(IcsFiles.Duration1).SetName("Duration1"); - yield return new TestCaseData(IcsFiles.Encoding1).SetName("Encoding1"); - yield return new TestCaseData(IcsFiles.Event1).SetName("Event1"); - yield return new TestCaseData(IcsFiles.Event2).SetName("Event2"); - yield return new TestCaseData(IcsFiles.Event3).SetName("Event3"); - yield return new TestCaseData(IcsFiles.Event4).SetName("Event4"); - yield return new TestCaseData(IcsFiles.GeographicLocation1).SetName("GeographicLocation1"); - yield return new TestCaseData(IcsFiles.Language1).SetName("Language1"); - yield return new TestCaseData(IcsFiles.Language2).SetName("Language2"); - yield return new TestCaseData(IcsFiles.Language3).SetName("Language3"); - yield return new TestCaseData(IcsFiles.TimeZone1).SetName("TimeZone1"); - yield return new TestCaseData(IcsFiles.TimeZone2).SetName("TimeZone2"); - yield return new TestCaseData(IcsFiles.TimeZone3).SetName("TimeZone3"); - yield return new TestCaseData(IcsFiles.XProperty1).SetName("XProperty1"); - yield return new TestCaseData(IcsFiles.XProperty2).SetName("XProperty2"); - } - - private static readonly DateTime _now = DateTime.Now; - private static readonly DateTime _later = _now.AddHours(1); - - private static CalendarEvent GetSimpleEvent() => new CalendarEvent - { - DtStart = new CalDateTime(_now), - DtEnd = new CalDateTime(_later), - Duration = TimeSpan.FromHours(1), - }; - - private static string SerializeEvent(CalendarEvent e) => new CalendarSerializer().SerializeToString(new Calendar { Events = { e } }); - - [Test] - public void EventUid_Tests() - { - var e = GetSimpleEvent(); - e.Uid = "Hello"; - var copy = e.Copy(); - Assert.That(copy.Uid, Is.EqualTo(e.Uid)); - - copy.Uid = "Goodbye"; - - const string uidPattern = "UID:"; - var serializedOrig = SerializeEvent(e); - Assert.That(Regex.Matches(serializedOrig, uidPattern), Has.Count.EqualTo(1)); - - var serializedCopy = SerializeEvent(copy); - Assert.That(Regex.Matches(serializedCopy, uidPattern), Has.Count.EqualTo(1)); - } - } -} diff --git a/Ical.Net.Tests/DeserializationTests.cs b/Ical.Net.Tests/DeserializationTests.cs index a4e5350e..e32d6fb0 100644 --- a/Ical.Net.Tests/DeserializationTests.cs +++ b/Ical.Net.Tests/DeserializationTests.cs @@ -229,8 +229,8 @@ public void Encoding2() var evt = iCal.Events.First(); Assert.That( - evt.Attachments[0].ToString(), - Is.EqualTo("This is a test to try out base64 encoding without being too large.\r\n" + + evt.Attachments[0].ToString(), + Is.EqualTo("This is a test to try out base64 encoding without being too large.\r\n" + "This is a test to try out base64 encoding without being too large.\r\n" + "This is a test to try out base64 encoding without being too large.\r\n" + "This is a test to try out base64 encoding without being too large.\r\n" + @@ -242,7 +242,7 @@ public void Encoding2() "This is a test to try out base64 encoding without being too large.\r\n" + "This is a test to try out base64 encoding without being too large.\r\n" + "This is a test to try out base64 encoding without being too large."), - "Attached value does not match."); + "Attached value does not match."); } [Test] diff --git a/Ical.Net.Tests/Ical.Net.Tests.csproj b/Ical.Net.Tests/Ical.Net.Tests.csproj index b6d93fdd..dbb76431 100644 --- a/Ical.Net.Tests/Ical.Net.Tests.csproj +++ b/Ical.Net.Tests/Ical.Net.Tests.csproj @@ -3,6 +3,7 @@ net8.0;net6.0;net48 true ..\IcalNetStrongnameKey.snk + latest diff --git a/Ical.Net.Tests/RecurrenceTests.cs b/Ical.Net.Tests/RecurrenceTests.cs index 22a49157..ec52cef1 100644 --- a/Ical.Net.Tests/RecurrenceTests.cs +++ b/Ical.Net.Tests/RecurrenceTests.cs @@ -37,9 +37,10 @@ int eventIndex .OrderBy(o => o.Period.StartTime) .ToList(); - Assert.That(occurrences, - Has.Count.EqualTo(dateTimes.Length), - "There should be exactly " + dateTimes.Length + " occurrences; there were " + occurrences.Count); + Assert.That( + occurrences, +Has.Count.EqualTo(dateTimes.Length), + "There should be exactly " + dateTimes.Length + " occurrences; there were " + occurrences.Count); if (evt.RecurrenceRules.Count > 0) { diff --git a/Ical.Net.Tests/SerializationTests.cs b/Ical.Net.Tests/SerializationTests.cs index 1516830c..a633c642 100644 --- a/Ical.Net.Tests/SerializationTests.cs +++ b/Ical.Net.Tests/SerializationTests.cs @@ -117,7 +117,7 @@ public static string InspectSerializedSection(string serialized, string sectionN var end = serialized.IndexOf(searchFor, begin); Assert.That(end, Is.Not.EqualTo(-1), () => string.Format(notFound, searchFor)); - var searchRegion = serialized.Substring(begin, end - begin + 1); + var searchRegion = serialized.Substring(begin, end - begin + searchFor.Length); foreach (var e in elements) { @@ -284,14 +284,14 @@ public void EventPropertiesSerialized() { new Attendee("MAILTO:james@example.com") { - CommonName = "James James", + CommonName = "James", Role = ParticipationRole.RequiredParticipant, Rsvp = true, ParticipationStatus = EventParticipationStatus.Tentative }, new Attendee("MAILTO:mary@example.com") { - CommonName = "Mary Mary", + CommonName = "Mary", Role = ParticipationRole.RequiredParticipant, Rsvp = true, ParticipationStatus = EventParticipationStatus.Accepted @@ -301,7 +301,6 @@ public void EventPropertiesSerialized() [Test, Category("Serialization")] public void AttendeesSerialized() { - //ToDo: This test is broken as of 2016-07-13 var cal = new Calendar { Method = "REQUEST", @@ -310,12 +309,14 @@ public void AttendeesSerialized() var evt = AttendeeTest.VEventFactory(); cal.Events.Add(evt); - const string org = "MAILTO:james@example.com"; + // new Uri() creates lowercase for the "MAILTO:" part + // according to the RFC 2368 specification + const string org = "MAILTO:james@example.com"; evt.Organizer = new Organizer(org); evt.Attendees.AddRange(_attendees); - // However a bug, when a participation value is changed, ultimately re-serialises as an array (PARTSTAT=ACCEPTED,DECLINED) + // Changing the ParticipationStatus just keeps the last status evt.Attendees[0].ParticipationStatus = EventParticipationStatus.Declined; var serializer = new CalendarSerializer(); @@ -325,7 +326,7 @@ public void AttendeesSerialized() foreach (var a in evt.Attendees) { - var vals = GetValues(vEvt, "ATTENDEE", a.Value.OriginalString); + var vals = GetValues(vEvt, "ATTENDEE", a.Value.ToString()); foreach (var v in new Dictionary { ["CN"] = a.CommonName, @@ -338,7 +339,7 @@ public void AttendeesSerialized() Assert.Multiple(() => { Assert.That(vals.ContainsKey(v.Key), Is.True, $"could not find key '{v.Key}'"); - Assert.That(vals[v.Key], Is.EqualTo(v.Value), $"ATENDEE prop '{v.Key}' differ"); + Assert.That(vals[v.Key], Is.EqualTo(v.Value), $"ATTENDEE prop '{v.Key}' differ"); }); } } diff --git a/Ical.Net/CalendarComponents/Alarm.cs b/Ical.Net/CalendarComponents/Alarm.cs index 6f0adb42..4d8fda1f 100644 --- a/Ical.Net/CalendarComponents/Alarm.cs +++ b/Ical.Net/CalendarComponents/Alarm.cs @@ -10,7 +10,6 @@ namespace Ical.Net.CalendarComponents /// public class Alarm : CalendarComponent { - //ToDo: Implement IEquatable public virtual string Action { get => Properties.Get(AlarmAction.Key); @@ -177,4 +176,4 @@ protected virtual void AddRepeatedItems() } } } -} \ No newline at end of file +} diff --git a/Ical.Net/CalendarComponents/CalendarComponent.cs b/Ical.Net/CalendarComponents/CalendarComponent.cs index cf5b77e1..a34e1e25 100644 --- a/Ical.Net/CalendarComponents/CalendarComponent.cs +++ b/Ical.Net/CalendarComponents/CalendarComponent.cs @@ -37,6 +37,7 @@ protected override void OnDeserializing(StreamingContext context) Initialize(); } + /// public override void CopyFrom(ICopyable obj) { base.CopyFrom(obj); @@ -50,7 +51,8 @@ public override void CopyFrom(ICopyable obj) Properties.Clear(); foreach (var p in c.Properties) { - Properties.Add(p); + // Uses CalendarObjectBase.Copy() for a deep copy + Properties.Add(p.Copy()); } } diff --git a/Ical.Net/CalendarComponents/CalendarEvent.cs b/Ical.Net/CalendarComponents/CalendarEvent.cs index 4f3243cf..272c9f9a 100644 --- a/Ical.Net/CalendarComponents/CalendarEvent.cs +++ b/Ical.Net/CalendarComponents/CalendarEvent.cs @@ -161,8 +161,10 @@ public string Location /// /// Resources that will be used during the event. - /// Conference room #2 - /// Projector + /// To change existing values, assign a new . + /// Examples: + /// Conference room, Projector + /// /// public virtual IList Resources { diff --git a/Ical.Net/CalendarObject.cs b/Ical.Net/CalendarObject.cs index 486edfcb..5cf150db 100644 --- a/Ical.Net/CalendarObject.cs +++ b/Ical.Net/CalendarObject.cs @@ -30,11 +30,8 @@ public CalendarObject(int line, int col) : this() private void Initialize() { - //ToDo: I'm fairly certain this is ONLY used for null checking. If so, maybe it can just be a bool? CalendarObjectList is an empty object, and - //ToDo: its constructor parameter is ignored - _children = new CalendarObjectList(this); + _children = new CalendarObjectList(); _serviceProvider = new ServiceProvider(); - _children.ItemAdded += Children_ItemAdded; } @@ -61,10 +58,10 @@ public override bool Equals(object obj) public override int GetHashCode() => Name?.GetHashCode() ?? 0; + /// public override void CopyFrom(ICopyable c) { - var obj = c as ICalendarObject; - if (obj == null) + if (c is not ICalendarObject obj) { return; } @@ -74,12 +71,13 @@ public override void CopyFrom(ICopyable c) Parent = obj.Parent; Line = obj.Line; Column = obj.Column; - + // Add each child Children.Clear(); foreach (var child in obj.Children) { - this.AddChild(child); + // Add a deep copy of the child instead of the child itself + this.AddChild(child.Copy()); } } @@ -99,21 +97,22 @@ public override void CopyFrom(ICopyable c) public virtual string Name { get; set; } /// - /// Returns the that this DDayiCalObject belongs to. + /// Gets the object. + /// The setter must be implemented in a derived class. /// public virtual Calendar Calendar { get { ICalendarObject obj = this; - while (!(obj is Calendar) && obj.Parent != null) + while (obj is not Net.Calendar && obj.Parent != null) { obj = obj.Parent; } return obj as Calendar; } - protected set { } + protected set => throw new NotSupportedException(); } public virtual int Line { get; set; } diff --git a/Ical.Net/CalendarObjectBase.cs b/Ical.Net/CalendarObjectBase.cs index 03d28d96..b25d50c0 100644 --- a/Ical.Net/CalendarObjectBase.cs +++ b/Ical.Net/CalendarObjectBase.cs @@ -2,37 +2,33 @@ namespace Ical.Net { + // This class should be declared as abstract public class CalendarObjectBase : ICopyable, ILoadable { - private bool _mIsLoaded; - - public CalendarObjectBase() - { - _mIsLoaded = true; - } + private bool _mIsLoaded = true; /// - /// Copies values from the target object to the - /// current object. + /// Makes a deep copy of the source + /// to the current object. This method must be overridden in a derived class. /// - public virtual void CopyFrom(ICopyable c) { } + public virtual void CopyFrom(ICopyable obj) + { + throw new NotImplementedException("Must be implemented in a derived class."); + } /// - /// Creates a copy of the object. + /// Creates a deep copy of the object. /// - /// The copy of the object. + /// The copy of the object. public virtual T Copy() { var type = GetType(); var obj = Activator.CreateInstance(type) as ICopyable; + + if (obj is not T objOfT) return default(T); - // Duplicate our values - if (obj is T) - { - obj.CopyFrom(this); - return (T)obj; - } - return default(T); + obj.CopyFrom(this); + return objOfT; } public virtual bool IsLoaded => _mIsLoaded; diff --git a/Ical.Net/CalendarObjectList.cs b/Ical.Net/CalendarObjectList.cs index 7dc19f3c..8edcb2c3 100644 --- a/Ical.Net/CalendarObjectList.cs +++ b/Ical.Net/CalendarObjectList.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using Ical.Net.Collections; namespace Ical.Net @@ -5,8 +6,8 @@ namespace Ical.Net /// /// A collection of calendar objects. /// + [ExcludeFromCodeCoverage] public class CalendarObjectList : GroupedList, ICalendarObjectList { - public CalendarObjectList(ICalendarObject parent) {} } } \ No newline at end of file diff --git a/Ical.Net/CalendarParameter.cs b/Ical.Net/CalendarParameter.cs index f2c7a7a0..6b09314c 100644 --- a/Ical.Net/CalendarParameter.cs +++ b/Ical.Net/CalendarParameter.cs @@ -49,6 +49,7 @@ protected override void OnDeserializing(StreamingContext context) Initialize(); } + /// public override void CopyFrom(ICopyable c) { base.CopyFrom(c); diff --git a/Ical.Net/CalendarProperty.cs b/Ical.Net/CalendarProperty.cs index 8d60ea5d..294293fc 100644 --- a/Ical.Net/CalendarProperty.cs +++ b/Ical.Net/CalendarProperty.cs @@ -61,12 +61,12 @@ public virtual void AddParameter(CalendarParameter p) Parameters.Add(p); } + /// public override void CopyFrom(ICopyable obj) { base.CopyFrom(obj); - var p = obj as ICalendarProperty; - if (p == null) + if (obj is not ICalendarProperty p) { return; } @@ -124,7 +124,9 @@ public virtual void SetValue(IEnumerable values) { // Remove all previous values _values.Clear(); - var toAdd = values ?? Enumerable.Empty(); + // If the values are ICopyable, create a deep copy of each value, + // otherwise just add the value + var toAdd = values?.Select(x => (x as ICopyable)?.Copy() ?? x) ?? Enumerable.Empty(); _values.AddRange(toAdd); } diff --git a/Ical.Net/Collections/GroupedValueList.cs b/Ical.Net/Collections/GroupedValueList.cs index 72fb8233..19ce810b 100644 --- a/Ical.Net/Collections/GroupedValueList.cs +++ b/Ical.Net/Collections/GroupedValueList.cs @@ -27,8 +27,8 @@ public virtual void Set(TGroup group, IEnumerable values) // No matching item was found, add a new item to the list var obj = Activator.CreateInstance(typeof(TItem)) as TInterface; obj.Group = group; - Add(obj); obj.SetValue(values); + Add(obj); } public virtual TType Get(TGroup group) diff --git a/Ical.Net/DataTypes/Attachment.cs b/Ical.Net/DataTypes/Attachment.cs index ef2972ec..d21cb90c 100644 --- a/Ical.Net/DataTypes/Attachment.cs +++ b/Ical.Net/DataTypes/Attachment.cs @@ -14,7 +14,7 @@ namespace Ical.Net.DataTypes public class Attachment : EncodableDataType { public virtual Uri Uri { get; set; } - public virtual byte[] Data { get; } + public virtual byte[] Data { get; private set; } // private set for CopyFrom private Encoding _valueEncoding = System.Text.Encoding.UTF8; public virtual Encoding ValueEncoding @@ -71,8 +71,22 @@ public override string ToString() ? string.Empty : ValueEncoding.GetString(Data); - //ToDo: See if this can be deleted - public override void CopyFrom(ICopyable obj) { } + /// + public override void CopyFrom(ICopyable obj) + { + if (obj is not Attachment att) return; + base.CopyFrom(obj); + + Uri = att.Uri != null ? new Uri(att.Uri.ToString()) : null; + if (att.Data != null) + { + Data = new byte[att.Data.Length]; + Array.Copy(att.Data, Data, att.Data.Length); + } + + ValueEncoding = att.ValueEncoding; + FormatType = att.FormatType; + } protected bool Equals(Attachment other) { diff --git a/Ical.Net/DataTypes/Attendee.cs b/Ical.Net/DataTypes/Attendee.cs index 350f64ee..11c4c77f 100644 --- a/Ical.Net/DataTypes/Attendee.cs +++ b/Ical.Net/DataTypes/Attendee.cs @@ -242,9 +242,26 @@ public Attendee(string attendeeUri) Value = new Uri(attendeeUri); } - //ToDo: See if this can be deleted - public override void CopyFrom(ICopyable obj) { } + /// + public override void CopyFrom(ICopyable obj) + { + if (obj is not Attendee atn) return; + base.CopyFrom(obj); + + Value = new Uri(atn.Value.ToString()); + + // String assignments create new instances + CommonName = atn.CommonName; + ParticipationStatus = atn.ParticipationStatus; + Role = atn.Role; + Type = atn.Type; + Rsvp = atn.Rsvp; + + SentBy = atn.SentBy != null ? new Uri(atn.SentBy.ToString()) : null; + DirectoryEntry = atn.DirectoryEntry != null ? new Uri(atn.DirectoryEntry.ToString()) : null; + } + protected bool Equals(Attendee other) => Equals(SentBy, other.SentBy) && string.Equals(CommonName, other.CommonName, StringComparison.OrdinalIgnoreCase) && Equals(DirectoryEntry, other.DirectoryEntry) diff --git a/Ical.Net/DataTypes/CalDateTime.cs b/Ical.Net/DataTypes/CalDateTime.cs index a6a336ba..c149d779 100644 --- a/Ical.Net/DataTypes/CalDateTime.cs +++ b/Ical.Net/DataTypes/CalDateTime.cs @@ -131,6 +131,7 @@ public override ICalendarObject AssociatedObject } } + /// public override void CopyFrom(ICopyable obj) { base.CopyFrom(obj); @@ -144,6 +145,8 @@ public override void CopyFrom(ICopyable obj) _value = dt.Value; _hasDate = dt.HasDate; _hasTime = dt.HasTime; + // String assignments create new instances + TzId = dt.TzId; AssociateWith(dt); } diff --git a/Ical.Net/DataTypes/CalendarDataType.cs b/Ical.Net/DataTypes/CalendarDataType.cs index 622df5ce..a888b67f 100644 --- a/Ical.Net/DataTypes/CalendarDataType.cs +++ b/Ical.Net/DataTypes/CalendarDataType.cs @@ -129,13 +129,10 @@ public virtual string Language set => Parameters.Set("LANGUAGE", value); } - /// - /// Copies values from the target object to the - /// current object. - /// + /// public virtual void CopyFrom(ICopyable obj) { - if (!(obj is ICalendarDataType dt)) + if (obj is not ICalendarDataType dt) { return; } @@ -146,21 +143,18 @@ public virtual void CopyFrom(ICopyable obj) } /// - /// Creates a copy of the object. + /// Creates a deep copy of the object. /// - /// The copy of the object. + /// The copy of the object. public virtual T Copy() { var type = GetType(); var obj = Activator.CreateInstance(type) as ICopyable; - // Duplicate our values - if (obj is T) - { - obj.CopyFrom(this); - return (T)obj; - } - return default(T); + if (obj is not T o) return default(T); + + obj.CopyFrom(this); + return o; } public virtual IParameterCollection Parameters => _proxy; diff --git a/Ical.Net/DataTypes/FreeBusyEntry.cs b/Ical.Net/DataTypes/FreeBusyEntry.cs index 9aefeb80..a73d5373 100644 --- a/Ical.Net/DataTypes/FreeBusyEntry.cs +++ b/Ical.Net/DataTypes/FreeBusyEntry.cs @@ -17,12 +17,12 @@ public FreeBusyEntry(Period period, FreeBusyStatus status) Status = status; } + /// public override void CopyFrom(ICopyable obj) { base.CopyFrom(obj); - var fb = obj as FreeBusyEntry; - if (fb != null) + if (obj is FreeBusyEntry fb) { Status = fb.Status; } diff --git a/Ical.Net/DataTypes/GeographicLocation.cs b/Ical.Net/DataTypes/GeographicLocation.cs index a7d3ad69..9e5698da 100644 --- a/Ical.Net/DataTypes/GeographicLocation.cs +++ b/Ical.Net/DataTypes/GeographicLocation.cs @@ -28,7 +28,20 @@ public GeographicLocation(double latitude, double longitude) Longitude = longitude; } - public override void CopyFrom(ICopyable obj) { } + /// + public override void CopyFrom(ICopyable obj) + { + base.CopyFrom(obj); + + var geo = obj as GeographicLocation; + if (geo == null) + { + return; + } + + Latitude = geo.Latitude; + Longitude = geo.Longitude; + } public override string ToString() => Latitude.ToString("0.000000") + ";" + Longitude.ToString("0.000000"); diff --git a/Ical.Net/DataTypes/Organizer.cs b/Ical.Net/DataTypes/Organizer.cs index d63198e2..58374d07 100644 --- a/Ical.Net/DataTypes/Organizer.cs +++ b/Ical.Net/DataTypes/Organizer.cs @@ -85,12 +85,12 @@ public override bool Equals(object obj) public override int GetHashCode() => Value?.GetHashCode() ?? 0; + /// public override void CopyFrom(ICopyable obj) { base.CopyFrom(obj); - var o = obj as Organizer; - if (o != null) + if (obj is Organizer o) { Value = o.Value; } diff --git a/Ical.Net/DataTypes/Period.cs b/Ical.Net/DataTypes/Period.cs index 187d38d4..f5fefa33 100644 --- a/Ical.Net/DataTypes/Period.cs +++ b/Ical.Net/DataTypes/Period.cs @@ -44,17 +44,15 @@ public Period(IDateTime start, TimeSpan duration) EndTime = start.Add(duration); } + /// public override void CopyFrom(ICopyable obj) { base.CopyFrom(obj); - var p = obj as Period; - if (p == null) - { - return; - } - StartTime = p.StartTime; - EndTime = p.EndTime; + if (obj is not Period p) return; + + StartTime = p.StartTime?.Copy(); + EndTime = p.EndTime?.Copy(); Duration = p.Duration; } diff --git a/Ical.Net/DataTypes/PeriodList.cs b/Ical.Net/DataTypes/PeriodList.cs index 069f1420..5379bb0f 100644 --- a/Ical.Net/DataTypes/PeriodList.cs +++ b/Ical.Net/DataTypes/PeriodList.cs @@ -1,4 +1,4 @@ -using Ical.Net.Evaluation; +using Ical.Net.Evaluation; using Ical.Net.Serialization.DataTypes; using Ical.Net.Utility; using System; @@ -30,18 +30,22 @@ public PeriodList(string value) : this() CopyFrom(serializer.Deserialize(new StringReader(value)) as ICopyable); } + /// public override void CopyFrom(ICopyable obj) { base.CopyFrom(obj); - if (!(obj is PeriodList list)) + if (obj is not PeriodList list) { return; } foreach (var p in list) { - Add(p); + Add(p.Copy()); } + + // String assignments create new instances + TzId = list.TzId; } public override string ToString() => new PeriodListSerializer().SerializeToString(this); diff --git a/Ical.Net/DataTypes/RecurrencePattern.cs b/Ical.Net/DataTypes/RecurrencePattern.cs index 5bad96d1..bde5ebff 100644 --- a/Ical.Net/DataTypes/RecurrencePattern.cs +++ b/Ical.Net/DataTypes/RecurrencePattern.cs @@ -1,4 +1,4 @@ -using Ical.Net.Evaluation; +using Ical.Net.Evaluation; using Ical.Net.Serialization.DataTypes; using Ical.Net.Utility; using System; @@ -189,16 +189,15 @@ public override int GetHashCode() } } + /// public override void CopyFrom(ICopyable obj) { base.CopyFrom(obj); - if (!(obj is RecurrencePattern)) + if (obj is not RecurrencePattern r) { return; } - var r = (RecurrencePattern)obj; - Frequency = r.Frequency; Until = r.Until; Count = r.Count; diff --git a/Ical.Net/DataTypes/RequestStatus.cs b/Ical.Net/DataTypes/RequestStatus.cs index d930cf7c..85671688 100644 --- a/Ical.Net/DataTypes/RequestStatus.cs +++ b/Ical.Net/DataTypes/RequestStatus.cs @@ -38,10 +38,11 @@ public RequestStatus(string value) : this() CopyFrom(serializer.Deserialize(new StringReader(value)) as ICopyable); } + /// public override void CopyFrom(ICopyable obj) { base.CopyFrom(obj); - if (!(obj is RequestStatus rs)) + if (obj is not RequestStatus rs) { return; } diff --git a/Ical.Net/DataTypes/StatusCode.cs b/Ical.Net/DataTypes/StatusCode.cs index 2e007e4e..2f55af6f 100644 --- a/Ical.Net/DataTypes/StatusCode.cs +++ b/Ical.Net/DataTypes/StatusCode.cs @@ -1,4 +1,4 @@ -using Ical.Net.Serialization.DataTypes; +using Ical.Net.Serialization.DataTypes; using Ical.Net.Utility; using System.IO; using System.Linq; @@ -45,15 +45,14 @@ public StatusCode(string value) : this() CopyFrom(serializer.Deserialize(new StringReader(value)) as ICopyable); } + /// public override void CopyFrom(ICopyable obj) { base.CopyFrom(obj); - if (obj is StatusCode) - { - var sc = (StatusCode)obj; - Parts = new int[sc.Parts.Length]; - sc.Parts.CopyTo(Parts, 0); - } + if (obj is not StatusCode statusCode) return; + + Parts = new int[statusCode.Parts.Length]; + statusCode.Parts.CopyTo(Parts, 0); } public override string ToString() => new StatusCodeSerializer().SerializeToString(this); diff --git a/Ical.Net/DataTypes/Trigger.cs b/Ical.Net/DataTypes/Trigger.cs index 96e2b112..ee9e8b66 100644 --- a/Ical.Net/DataTypes/Trigger.cs +++ b/Ical.Net/DataTypes/Trigger.cs @@ -1,4 +1,4 @@ -using Ical.Net.Serialization.DataTypes; +using Ical.Net.Serialization.DataTypes; using System; using System.IO; @@ -73,16 +73,16 @@ public Trigger(string value) : this() CopyFrom(serializer.Deserialize(new StringReader(value)) as ICopyable); } + /// public override void CopyFrom(ICopyable obj) { base.CopyFrom(obj); - if (!(obj is Trigger)) + if (obj is not Trigger t) { return; } - - var t = (Trigger)obj; - DateTime = t.DateTime; + + DateTime = t.DateTime?.Copy(); Duration = t.Duration; Related = t.Related; } diff --git a/Ical.Net/DataTypes/UTCOffset.cs b/Ical.Net/DataTypes/UTCOffset.cs index a0f25488..b08eaa45 100644 --- a/Ical.Net/DataTypes/UTCOffset.cs +++ b/Ical.Net/DataTypes/UTCOffset.cs @@ -1,4 +1,4 @@ -using Ical.Net.Serialization.DataTypes; +using Ical.Net.Serialization.DataTypes; using System; namespace Ical.Net.DataTypes @@ -8,7 +8,7 @@ namespace Ical.Net.DataTypes /// public class UtcOffset : EncodableDataType { - public TimeSpan Offset { get; } + public TimeSpan Offset { get; private set; } public bool Positive => Offset >= TimeSpan.Zero; @@ -60,5 +60,16 @@ public override bool Equals(object obj) public override int GetHashCode() => Offset.GetHashCode(); public override string ToString() => (Positive ? "+" : "-") + Hours.ToString("00") + Minutes.ToString("00") + (Seconds != 0 ? Seconds.ToString("00") : string.Empty); + + /// + public override void CopyFrom(ICopyable obj) + { + base.CopyFrom(obj); + + if (obj is UtcOffset o) + { + Offset = o.Offset; + } + } } } \ No newline at end of file diff --git a/Ical.Net/DataTypes/WeekDay.cs b/Ical.Net/DataTypes/WeekDay.cs index de477d12..abb7f073 100644 --- a/Ical.Net/DataTypes/WeekDay.cs +++ b/Ical.Net/DataTypes/WeekDay.cs @@ -1,4 +1,4 @@ -using Ical.Net.Serialization.DataTypes; +using Ical.Net.Serialization.DataTypes; using System; using System.IO; @@ -49,37 +49,34 @@ public override bool Equals(object obj) public override int GetHashCode() => Offset.GetHashCode() ^ DayOfWeek.GetHashCode(); + /// public override void CopyFrom(ICopyable obj) { base.CopyFrom(obj); - if (obj is WeekDay) - { - var bd = (WeekDay)obj; - Offset = bd.Offset; - DayOfWeek = bd.DayOfWeek; - } + if (obj is not WeekDay weekday) return; + + Offset = weekday.Offset; + DayOfWeek = weekday.DayOfWeek; } public int CompareTo(object obj) { - WeekDay bd = null; - if (obj is string) + var weekday = obj switch { - bd = new WeekDay(obj.ToString()); - } - else if (obj is WeekDay) - { - bd = (WeekDay)obj; - } + string => new WeekDay(obj.ToString()), + WeekDay day => day, + _ => null + }; - if (bd == null) + if (weekday == null) { throw new ArgumentException(); } - var compare = DayOfWeek.CompareTo(bd.DayOfWeek); + + var compare = DayOfWeek.CompareTo(weekday.DayOfWeek); if (compare == 0) { - compare = Offset.CompareTo(bd.Offset); + compare = Offset.CompareTo(weekday.Offset); } return compare; } diff --git a/Ical.Net/ICopyable.cs b/Ical.Net/ICopyable.cs index 9b169a54..e20c7d55 100644 --- a/Ical.Net/ICopyable.cs +++ b/Ical.Net/ICopyable.cs @@ -3,16 +3,18 @@ public interface ICopyable { /// - /// Copies all relevant fields/properties from - /// the target object to the current one. + /// (Deep) copies all relevant members from + /// the source object to the current one. + /// + /// If an object cannot use the implementation of a base class, + /// it must override this method and implement the copy logic itself. /// void CopyFrom(ICopyable obj); /// - /// Returns a deep copy of the current object. For the most part, this is only necessary when working with mutable reference types, - /// (i.e. iCalDateTime). For most other types, it's unnecessary overhead. The pattern that identifies whether it's necessary to copy - /// or not is whether arithmetic operations mutate fields or properties. iCalDateTime is a good example where + and - would otherwise - /// change the Value of the underlying DateTime. + /// Returns a deep copy of the current object, mostly by using the method + /// of the object when it is overridden, otherwise is used the implementation of the base class. + /// This is necessary when working with mutable reference types. /// T Copy(); } diff --git a/Ical.Net/Ical.Net.csproj b/Ical.Net/Ical.Net.csproj index c4b732c6..335d33f4 100644 --- a/Ical.Net/Ical.Net.csproj +++ b/Ical.Net/Ical.Net.csproj @@ -3,6 +3,7 @@ net8.0;net6.0;netstandard2.1;netstandard2.0 true ..\IcalNetStrongnameKey.snk + latest diff --git a/Ical.Net/Serialization/DataTypes/UtcOffsetSerializer.cs b/Ical.Net/Serialization/DataTypes/UtcOffsetSerializer.cs index 64ead0e4..1ed81690 100644 --- a/Ical.Net/Serialization/DataTypes/UtcOffsetSerializer.cs +++ b/Ical.Net/Serialization/DataTypes/UtcOffsetSerializer.cs @@ -16,16 +16,13 @@ public UtcOffsetSerializer(SerializationContext ctx) : base(ctx) { } public override string SerializeToString(object obj) { - var offset = obj as UtcOffset; - if (offset != null) - { - var value = (offset.Positive ? "+" : "-") + offset.Hours.ToString("00") + offset.Minutes.ToString("00") + - (offset.Seconds != 0 ? offset.Seconds.ToString("00") : string.Empty); + if (obj is not UtcOffset offset) return null; - // Encode the value as necessary - return Encode(offset, value); - } - return null; + var value = (offset.Positive ? "+" : "-") + offset.Hours.ToString("00") + offset.Minutes.ToString("00") + + (offset.Seconds != 0 ? offset.Seconds.ToString("00") : string.Empty); + + // Encode the value as necessary + return Encode(offset, value); } internal static readonly Regex DecodeOffset = new Regex(@"(\+|-)(\d{2})(\d{2})(\d{2})?", RegexOptions.Compiled | RegexOptions.IgnoreCase, RegexDefaults.Timeout);