Update Reference Sources to .NET Framework 4.6.1
[mono.git] / mcs / class / referencesource / System.Data.Linq / ChangeConflicts.cs
1 using System;
2 using System.Collections;
3 using System.Collections.Generic;
4 using System.Collections.ObjectModel;
5 using System.Text;
6 using System.Reflection;
7 using System.Linq;
8 using System.Diagnostics;
9
10 namespace System.Data.Linq {
11     using System.Data.Linq.Mapping;
12     using System.Data.Linq.Provider;
13     using System.Diagnostics.CodeAnalysis;
14
15     public sealed class ChangeConflictCollection : ICollection<ObjectChangeConflict>, ICollection, IEnumerable<ObjectChangeConflict>, IEnumerable {
16         private List<ObjectChangeConflict> conflicts;
17
18         internal ChangeConflictCollection() {
19             this.conflicts = new List<ObjectChangeConflict>();
20         }
21
22         /// <summary>
23         /// The number of conflicts in the collection
24         /// </summary>
25         public int Count {
26             get { return this.conflicts.Count; }
27         }
28
29         public ObjectChangeConflict this[int index] {
30             get { return this.conflicts[index]; }
31         }
32
33         bool ICollection<ObjectChangeConflict>.IsReadOnly {
34             get { return true; }
35         }
36
37         void ICollection<ObjectChangeConflict>.Add(ObjectChangeConflict item) {
38             throw Error.CannotAddChangeConflicts();
39         }
40
41         /// <summary>
42         /// Removes the specified conflict from the collection.
43         /// </summary>
44         /// <param name="item">The conflict to remove</param>
45         /// <returns></returns>
46         public bool Remove(ObjectChangeConflict item) {
47             return this.conflicts.Remove(item);
48         }
49
50         /// <summary>
51         /// Removes all conflicts from the collection
52         /// </summary>
53         public void Clear() {
54             this.conflicts.Clear();
55         }
56
57         /// <summary>
58         /// Returns true if the specified conflict is a member of the collection.
59         /// </summary>
60         /// <param name="item"></param>
61         /// <returns></returns>
62         public bool Contains(ObjectChangeConflict item) {
63             return this.conflicts.Contains(item);
64         }
65
66         public void CopyTo(ObjectChangeConflict[] array, int arrayIndex) {
67             this.conflicts.CopyTo(array, arrayIndex);
68         }
69
70         /// <summary>
71         /// Returns the enumerator for the collection.
72         /// </summary>
73         /// <returns></returns>
74         public IEnumerator<ObjectChangeConflict> GetEnumerator() {
75             return this.conflicts.GetEnumerator();
76         }
77
78         IEnumerator IEnumerable.GetEnumerator() {
79             return this.conflicts.GetEnumerator();
80         }
81
82         bool ICollection.IsSynchronized {
83             get { return false; }
84         }
85
86         object ICollection.SyncRoot {
87             get { return null; }
88         }
89
90         void ICollection.CopyTo(Array array, int index) {
91             ((ICollection)this.conflicts).CopyTo(array, index);
92         }
93
94         /// <summary>
95         /// Resolves all conflicts in the collection using the specified strategy.
96         /// </summary>
97         /// <param name="mode">The strategy to use to resolve the conflicts.</param>
98         public void ResolveAll(RefreshMode mode) {
99             this.ResolveAll(mode, true);
100         }
101
102         /// <summary>
103         /// Resolves all conflicts in the collection using the specified strategy.
104         /// </summary>
105         /// <param name="mode">The strategy to use to resolve the conflicts.</param>
106         /// <param name="autoResolveDeletes">If true conflicts resulting from the modified
107         /// object no longer existing in the database will be automatically resolved.</param>
108         public void ResolveAll(RefreshMode mode, bool autoResolveDeletes) {
109             foreach (ObjectChangeConflict c in this.conflicts) {
110                 if (!c.IsResolved) {
111                     c.Resolve(mode, autoResolveDeletes);
112                 }
113             }
114         }
115
116         internal void Fill(List<ObjectChangeConflict> conflictList) {
117             this.conflicts = conflictList;
118         }
119     }
120
121     internal sealed class ChangeConflictSession {
122         private DataContext context;
123         private DataContext refreshContext;
124
125         internal ChangeConflictSession(DataContext context) {
126             this.context = context;
127         }
128
129         internal DataContext Context {
130             get { return this.context; }
131         }
132
133         internal DataContext RefreshContext {
134             get {
135                 if (this.refreshContext == null) {
136                     this.refreshContext = this.context.CreateRefreshContext();
137                 }
138                 return this.refreshContext;
139             }
140         }
141     }
142
143     /// <summary>
144     /// Represents an update with one or more optimistic concurrency conflicts.
145     /// </summary>
146     public sealed class ObjectChangeConflict {
147         private ChangeConflictSession session;
148         private TrackedObject trackedObject;
149         private bool isResolved;
150         private ReadOnlyCollection<MemberChangeConflict> memberConflicts;
151         private object database;
152         private object original;
153         private bool? isDeleted;
154
155         /// <summary>
156         /// Constructor.
157         /// </summary>
158         /// <param name="session">The session in which the conflicts occurred.</param>
159         /// <param name="trackedObject">The tracked item in conflict.</param>
160         internal ObjectChangeConflict(ChangeConflictSession session, TrackedObject trackedObject) {
161             this.session = session;
162             this.trackedObject = trackedObject;
163             this.original = trackedObject.CreateDataCopy(trackedObject.Original);
164         }
165
166         /// <summary>
167         /// Constructor.
168         /// </summary>
169         /// <param name="session">The session in which the conflicts occurred.</param>
170         /// <param name="trackedObject">The tracked item in conflict.</param>
171         /// <param name="isDeleted">True if the item in conflict no longer exists in the database.</param>
172         internal ObjectChangeConflict(ChangeConflictSession session, TrackedObject trackedObject, bool isDeleted)
173             : this(session, trackedObject) {
174             this.isDeleted = isDeleted;
175         }
176
177         internal ChangeConflictSession Session {
178             get { return this.session; }
179         }
180
181         internal TrackedObject TrackedObject {
182             get { return this.trackedObject; }
183         }
184
185         /// <summary>
186         /// The object in conflict.
187         /// </summary>
188         public object Object {
189             get { return this.trackedObject.Current; }
190         }
191
192         /// <summary>
193         /// An instance containing the baseline original values used to perform the concurrency check.
194         /// </summary>
195         internal object Original {
196             get { return this.original; }
197         }
198
199         /// <summary>
200         /// True if the conflicts for this object have already been resovled.
201         /// </summary>
202         public bool IsResolved {
203             get { return this.isResolved; }
204         }
205        
206         /// <summary>
207         /// True if the object in conflict has been deleted from the database.
208         /// </summary>
209         public bool IsDeleted {
210             get { 
211                 if (this.isDeleted.HasValue) {
212                     return this.isDeleted.Value;
213                 }
214                 return (this.Database == null); 
215             }
216         }
217
218         /// <summary>
219         /// An instance containing the most recent values from the database
220         /// </summary>
221         internal object Database {
222             get {
223                 if (this.database == null) {
224                     // use the 'refresh' context to retrieve the current database state
225                     DataContext ctxt = this.session.RefreshContext;
226                     object[] keyValues = CommonDataServices.GetKeyValues(this.trackedObject.Type, this.original);
227                     this.database = ctxt.Services.GetObjectByKey(this.trackedObject.Type, keyValues);
228                 }
229                 return this.database;
230             }
231         }
232
233         /// <summary>
234         /// Resolve member conflicts keeping current values and resetting the baseline 'Original' values
235         /// to match the more recent 'Database' values.
236         /// </summary>
237         public void Resolve() {
238             this.Resolve(RefreshMode.KeepCurrentValues, true);
239         }
240
241         /// <summary>
242         /// Resolve member conflicts using the mode specified and resetting the baseline 'Original' values
243         /// to match the more recent 'Database' values.
244         /// </summary>  
245         /// <param name="refreshMode">The mode that determines how the current values are 
246         /// changed in order to resolve the conflict</param>
247         public void Resolve(RefreshMode refreshMode) {
248             this.Resolve(refreshMode, false);
249         }
250
251         /// <summary>
252         /// Resolve member conflicts using the mode specified and resetting the baseline 'Original' values
253         /// to match the more recent 'Database' values.
254         /// </summary>
255         /// <param name="refreshMode">The mode that determines how the current values are 
256         /// changed in order to resolve the conflict</param>
257         /// <param name="autoResolveDeletes">If true conflicts resulting from the modified
258         /// object no longer existing in the database will be automatically resolved.</param>
259         public void Resolve(RefreshMode refreshMode, bool autoResolveDeletes) {
260             if (autoResolveDeletes && this.IsDeleted) {
261                 this.ResolveDelete();
262             }
263             else {
264                 // We make these calls explicity rather than simply calling
265                 // DataContext.Refresh (which does virtually the same thing)
266                 // since we want to cache the database value read.
267                 if (this.Database == null) {
268                     throw Error.RefreshOfDeletedObject();
269                 }
270                 trackedObject.Refresh(refreshMode, this.Database);
271                 this.isResolved = true;
272             }
273         }
274
275         /// <summary>
276         /// Resolve a conflict where we have updated an entity that no longer exists
277         /// in the database.
278         /// </summary>
279         private void ResolveDelete() {
280             Debug.Assert(this.IsDeleted);
281             // If the user is attempting to update an entity that no longer exists 
282             // in the database, we first need to [....] the delete into the local cache.
283             if (!trackedObject.IsDeleted) {
284                 trackedObject.ConvertToDeleted();
285             }
286
287             // As the object have been deleted, it needs to leave the cache
288             this.Session.Context.Services.RemoveCachedObjectLike(trackedObject.Type, trackedObject.Original);
289
290             // Now that our cache is in [....], we accept the changes
291             this.trackedObject.AcceptChanges();
292             this.isResolved = true;
293         }
294
295         /// <summary>
296         /// Returns a collection of all member conflicts that caused the update to fail.
297         /// </summary>       
298         public ReadOnlyCollection<MemberChangeConflict> MemberConflicts {
299             get {
300                 if (this.memberConflicts == null) {
301                     var list = new List<MemberChangeConflict>();
302                     if (this.Database != null) {
303                         // determine which members are in conflict                   
304                         foreach (MetaDataMember metaMember in trackedObject.Type.PersistentDataMembers) {
305                             if (!metaMember.IsAssociation && this.HasMemberConflict(metaMember)) {
306                                 list.Add(new MemberChangeConflict(this, metaMember));
307                             }
308                         }
309                     }
310                     this.memberConflicts = list.AsReadOnly();
311                 }
312                 return this.memberConflicts;
313             }
314         }
315
316         private bool HasMemberConflict(MetaDataMember member) {
317             object oValue = member.StorageAccessor.GetBoxedValue(this.original);
318             if (!member.DeclaringType.Type.IsAssignableFrom(this.database.GetType())) {
319                 return false;
320             }
321             object dValue = member.StorageAccessor.GetBoxedValue(this.database);
322             return !this.AreEqual(member, oValue, dValue);
323         }
324
325         private bool AreEqual(MetaDataMember member, object v1, object v2) {
326             if (v1 == null && v2 == null)
327                 return true;
328             if (v1 == null || v2 == null)
329                 return false;
330             if (member.Type == typeof(char[])) {
331                 return this.AreEqual((char[])v1, (char[])v2);
332             }
333             else if (member.Type == typeof(byte[])) {
334                 return this.AreEqual((byte[])v1, (byte[])v2);
335             }
336             else {
337                 return object.Equals(v1, v2);
338             }
339         }
340
341         [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification="Unknown reason.")]
342         private bool AreEqual(char[] a1, char[] a2) {
343             if (a1.Length != a2.Length)
344                 return false;
345             for (int i = 0, n = a1.Length; i < n; i++) {
346                 if (a1[i] != a2[i])
347                     return false;
348             }
349             return true;
350         }
351
352         [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification="Unknown reason.")]
353         private bool AreEqual(byte[] a1, byte[] a2) {
354             if (a1.Length != a2.Length)
355                 return false;
356             for (int i = 0, n = a1.Length; i < n; i++) {
357                 if (a1[i] != a2[i])
358                     return false;
359             }
360             return true;
361         }
362
363         internal void OnMemberResolved() {
364             if (!this.IsResolved) {
365                 int nResolved = this.memberConflicts.AsEnumerable().Count(m => m.IsResolved);
366                 if (nResolved == this.memberConflicts.Count) {
367                     this.Resolve(RefreshMode.KeepCurrentValues, false);
368                 }
369             }
370         }
371     }
372
373     /// <summary>
374     /// Represents a single optimistic concurrency member conflict.
375     /// </summary>
376     public sealed class MemberChangeConflict {
377         private ObjectChangeConflict conflict;
378         private MetaDataMember metaMember;
379         private object originalValue;
380         private object databaseValue;
381         private object currentValue;
382         bool isResolved;
383
384         internal MemberChangeConflict(ObjectChangeConflict conflict, MetaDataMember metaMember) {
385             this.conflict = conflict;            
386             this.metaMember = metaMember;
387             this.originalValue = metaMember.StorageAccessor.GetBoxedValue(conflict.Original);
388             this.databaseValue = metaMember.StorageAccessor.GetBoxedValue(conflict.Database);
389             this.currentValue = metaMember.StorageAccessor.GetBoxedValue(conflict.TrackedObject.Current);
390         }
391
392         /// <summary>
393         /// The previous client value.
394         /// </summary>
395         public object OriginalValue {
396             get { return this.originalValue; }
397         }
398
399         /// <summary>
400         /// The current database value.
401         /// </summary>
402         public object DatabaseValue {
403             get { return this.databaseValue; }
404         }
405
406         /// <summary>
407         /// The current client value.
408         /// </summary>
409         public object CurrentValue {
410             get { return this.currentValue; }
411         }
412
413         /// <summary>
414         /// MemberInfo for the member in conflict.
415         /// </summary>
416         public MemberInfo Member {
417             get { return this.metaMember.Member; }
418         }
419
420         /// <summary>
421         /// Updates the current value to the specified value.
422         /// </summary>       
423         public void Resolve(object value) {
424             this.conflict.TrackedObject.RefreshMember(this.metaMember, RefreshMode.OverwriteCurrentValues, value);
425             this.isResolved = true;
426             this.conflict.OnMemberResolved();
427         }
428
429         /// <summary>
430         /// Updates the current value using the specified strategy.
431         /// </summary>        
432         public void Resolve(RefreshMode refreshMode) {
433             this.conflict.TrackedObject.RefreshMember(this.metaMember, refreshMode, this.databaseValue);
434             this.isResolved = true;
435             this.conflict.OnMemberResolved();
436         }
437
438         /// <summary>
439         /// True if the value was modified by the client.
440         /// </summary>
441         public bool IsModified {
442             get { return this.conflict.TrackedObject.HasChangedValue(this.metaMember); }
443         }
444
445         /// <summary>
446         /// True if the member conflict has been resolved.
447         /// </summary>
448         public bool IsResolved {
449             get { return this.isResolved; }
450         }
451     }
452 }