-
Notifications
You must be signed in to change notification settings - Fork 0
/
EntityCoder.cs
405 lines (352 loc) · 16.5 KB
/
EntityCoder.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Rock;
using Rock.Attribute;
using Rock.Data;
using Rock.Model;
namespace EntityCoding
{
/// <summary>
/// A helper class for importing / exporting entities into and out of Rock.
/// </summary>
public class EntityCoder : CodingHelper
{
#region Properties
/// <summary>
/// The list of entities that are queued up to be encoded. This is
/// an ordered list and the entities will be encoded/decoded in this
/// order.
/// </summary>
public List<QueuedEntity> Entities { get; private set; }
#endregion
#region Constructors
/// <summary>
/// Initialize a new Helper object for facilitating the export/import of entities.
/// </summary>
/// <param name="rockContext">The RockContext to work in when exporting or importing.</param>
public EntityCoder( RockContext rockContext )
: base( rockContext )
{
Entities = new List<QueuedEntity>();
}
#endregion
#region Methods
/// <summary>
/// Adds a root/primary entity to the queue list.
/// </summary>
/// <param name="entity">The entity that is to be included in the export.</param>
/// <param name="exporter">The exporter that will handle processing for this entity.</param>
public void EnqueueEntity( IEntity entity, IExporter exporter )
{
EnqueueEntity( entity, new EntityPath(), true, exporter );
}
/// <summary>
/// Process the queued list of entities that are waiting to be encoded. This
/// encodes all entities, generates new Guids for any entities that need them,
/// and then maps all references to the new Guids.
/// </summary>
/// <param name="guidEvaluation">A function that is called for each entity to determine if it needs a new Guid or not.</param>
/// <returns>A DataContainer that is ready for JSON export.</returns>
public ExportedEntitiesContainer GetExportedEntities()
{
var container = new ExportedEntitiesContainer();
//
// Find out if we need to give new Guid values to any entities.
//
foreach ( var queuedEntity in Entities )
{
queuedEntity.EncodedEntity = Export( queuedEntity );
if ( queuedEntity.ReferencePaths[0].Count == 0 || queuedEntity.RequiresNewGuid )
{
queuedEntity.EncodedEntity.GenerateNewGuid = true;
}
}
//
// Convert to a data container.
//
foreach ( var queuedEntity in Entities )
{
container.Entities.Add( queuedEntity.EncodedEntity );
if ( queuedEntity.ReferencePaths.Count == 1 && queuedEntity.ReferencePaths[0].Count == 0 )
{
container.RootEntities.Add( queuedEntity.EncodedEntity.Guid );
}
}
return container;
}
/// <summary>
/// Export the given entity into an EncodedEntity object. This can be used later to
/// reconstruct the entity.
/// </summary>
/// <param name="entity">The entity to be exported.</param>
/// <returns>The exported data that can be imported.</returns>
protected EncodedEntity Export( QueuedEntity queuedEntity )
{
EncodedEntity encodedEntity = new EncodedEntity();
Type entityType = GetEntityType( queuedEntity.Entity );
encodedEntity.Guid = queuedEntity.Entity.Guid;
encodedEntity.EntityType = entityType.FullName;
//
// Generate the standard properties and references.
//
foreach ( var property in GetEntityProperties( queuedEntity.Entity ) )
{
//
// Don't encode IEntity properties, we should have the Id encoded instead.
//
if ( typeof( IEntity ).IsAssignableFrom( property.PropertyType ) )
{
continue;
}
//
// Don't encode IEnumerable properties. Those should be included as
// their own entities to be encoded later.
//
if ( property.PropertyType.GetInterface( "IEnumerable" ) != null &&
property.PropertyType.GetGenericArguments().Length == 1 &&
typeof( IEntity ).IsAssignableFrom( property.PropertyType.GetGenericArguments()[0] ) )
{
continue;
}
encodedEntity.Properties.Add( property.Name, property.GetValue( queuedEntity.Entity ) );
}
//
// Run any post-process transforms.
//
foreach ( var processor in FindEntityProcessors( entityType ) )
{
var data = processor.ProcessExportedEntity( queuedEntity.Entity, encodedEntity, this );
if ( data != null )
{
encodedEntity.AddTransformData( processor.Identifier.ToString(), data );
}
}
//
// Generate the references to other entities.
//
foreach ( var x in queuedEntity.ReferencedEntities )
{
encodedEntity.MakePropertyIntoReference( x.Key, x.Value );
}
//
// Add in the user references.
//
encodedEntity.References.AddRange( queuedEntity.UserReferences.Values );
return encodedEntity;
}
/// <summary>
/// Adds an entity to the queue list. This provides circular reference checking as
/// well as ensuring that proper order is maintained for all entities.
/// </summary>
/// <param name="entity">The entity that is to be included in the export.</param>
/// <param name="path">The entity path that lead to this entity being encoded.</param>
/// <param name="entityIsCritical">True if the entity is critical, that is referenced directly.</param>
/// <param name="exporter">The exporter that will handle processing for this entity.</param>
protected void EnqueueEntity( IEntity entity, EntityPath path, bool entityIsCritical, IExporter exporter )
{
//
// These are system generated rows, we should never try to backup or restore them.
//
if ( entity.TypeName == "Rock.Model.EntityType" || entity.TypeName == "Rock.Model.FieldType" )
{
return;
}
//
// If the entity is already in our path that means we are beginning a circular
// reference so we can just ignore this one.
//
if ( path.Where( e => e.Entity.Guid == entity.Guid ).Any() )
{
return;
}
//
// Find the entities that this entity references, in other words entities that must
// exist before this one can be created, and queue them up.
//
var referencedEntities = FindReferencedEntities( entity, path, exporter );
foreach ( var r in referencedEntities )
{
if ( r.Value != null )
{
var refPath = path + new EntityPathComponent( entity, r.Key );
EnqueueEntity( r.Value, refPath, true, exporter );
}
}
//
// If we already know about the entity, add a reference to it and return.
//
var queuedEntity = Entities.Where( e => e.Entity.Guid == entity.Guid ).FirstOrDefault();
if ( queuedEntity == null )
{
queuedEntity = new QueuedEntity( entity, path.Clone() );
Entities.Add( queuedEntity );
}
else
{
//
// We have already visited this entity from the same parent. Not sure why we are here.
//
if ( path.Any() && queuedEntity.ReferencePaths.Where( r => r.Any() && r.Last().Entity.Guid == path.Last().Entity.Guid ).Any() )
{
return;
}
queuedEntity.AddReferencePath( path.Clone() );
}
//
// Add any new referenced properties/entities that may have been supplied for this
// entity, as it's possible that has changed based on the path we took to get here.
//
foreach ( var r in referencedEntities )
{
if ( !queuedEntity.ReferencedEntities.Any( e => e.Key == r.Key && e.Value == r.Value ) )
{
queuedEntity.ReferencedEntities.Add( new KeyValuePair<string, IEntity>( r.Key, r.Value ) );
}
}
//
// Mark the entity as critical if it's a root entity or is otherwise specified as critical.
//
if ( path.Count == 0 || entityIsCritical || exporter.IsPathCritical( path ) )
{
queuedEntity.IsCritical = true;
}
//
// Mark the entity as requiring a new Guid value if so indicated.
//
if ( exporter.DoesPathNeedNewGuid( path ) )
{
queuedEntity.RequiresNewGuid = true;
}
//
// Find the entities that this entity has as children. This is usually the many side
// of a one-to-many reference (such as a Workflow has many WorkflowActions, this would
// get a list of the WorkflowActions).
//
var children = FindChildEntities( entity, path, exporter );
children.ForEach( e => EnqueueEntity( e.Value, path + new EntityPathComponent( entity, e.Key ), false, exporter ) );
//
// Allow the exporter a chance to add custom reference values.
//
var userReferences = exporter.GetUserReferencesForPath( entity, path );
if ( userReferences != null && userReferences.Any() )
{
foreach ( var r in userReferences )
{
queuedEntity.UserReferences.AddOrReplace( r.Property, r );
}
}
}
/// <summary>
/// Find entities that this object references directly. These are entities that must be
/// created before this entity can be re-created.
/// </summary>
/// <param name="parentEntity">The parent entity whose references we need to find.</param>
/// <param name="path">The property path that led us to this final property.</param>
/// <param name="exporter">The object that handles filtering during an export process.</param>
/// <returns>A dictionary that identify the property names and entites to be followed.</returns>
protected List<KeyValuePair<string, IEntity>> FindReferencedEntities( IEntity parentEntity, EntityPath path, IExporter exporter )
{
var references = new List<KeyValuePair<string, IEntity>>();
var properties = GetEntityProperties( parentEntity );
//
// Take a stab at any properties that end in "Id" and likely reference another
// entity, such as a property called "WorkflowId" probably references the Workflow
// entity and should be linked by Guid.
//
foreach ( var property in properties )
{
if ( property.Name.EndsWith( "Id" ) && ( property.PropertyType == typeof( int ) || property.PropertyType == typeof( Nullable<int> ) ) )
{
var entityProperty = parentEntity.GetType().GetProperty( property.Name.Substring( 0, property.Name.Length - 2 ) );
if ( entityProperty == null || !typeof( IEntity ).IsAssignableFrom( entityProperty.PropertyType ) )
{
continue;
}
var value = ( IEntity ) entityProperty.GetValue( parentEntity );
if ( exporter.ShouldFollowPathProperty( path + new EntityPathComponent( parentEntity, property.Name ) ) )
{
references.Add( new KeyValuePair<string, IEntity>( property.Name, value ) );
}
else
{
references.Add( new KeyValuePair<string, IEntity>( property.Name, null ) );
}
}
}
//
// Allow for processors to adjust the list of children.
//
foreach ( var processor in FindEntityProcessors( GetEntityType( parentEntity ) ) )
{
processor.EvaluateReferencedEntities( parentEntity, references, this );
}
return references;
}
/// <summary>
/// Generate the list of entities that reference this parent entity. These are entities that
/// must be created after this entity has been created.
/// </summary>
/// <param name="parentEntity">The parent entity to find reverse-references to.</param>
/// <param name="path">The property path that led us to this final property.</param>
/// <param name="exporter">The object that handles filtering during an export process.</param>
/// <returns>A list of KeyValuePairs that identify the property names and entites to be followed.</returns>
protected List<KeyValuePair<string, IEntity>> FindChildEntities( IEntity parentEntity, EntityPath path, IExporter exporter )
{
List<KeyValuePair<string, IEntity>> children = new List<KeyValuePair<string, IEntity>>();
var properties = GetEntityProperties( parentEntity );
//
// Take a stab at any properties that are an ICollection<IEntity> and treat those
// as child entities.
//
foreach ( var property in properties )
{
if ( property.PropertyType.GetInterface( "IEnumerable" ) != null && property.PropertyType.GetGenericArguments().Length == 1 )
{
if ( typeof( IEntity ).IsAssignableFrom( property.PropertyType.GetGenericArguments()[0] ) && exporter.ShouldFollowPathProperty( path + new EntityPathComponent( parentEntity, property.Name ) ) )
{
IEnumerable childEntities = ( IEnumerable ) property.GetValue( parentEntity );
foreach ( IEntity childEntity in childEntities )
{
children.Add( new KeyValuePair<string, IEntity>( property.Name, childEntity ) );
}
}
}
}
//
// We also need to pull in any attribute values. We have to pull attributes as well
// since we might not have an actual value for that attribute yet and would need
// it to pull the default value and definition.
//
if ( parentEntity is IHasAttributes attributedEntity )
{
if ( attributedEntity.Attributes == null )
{
attributedEntity.LoadAttributes( RockContext );
}
foreach ( var item in attributedEntity.Attributes )
{
var attrib = new AttributeService( RockContext ).Get( item.Value.Guid );
children.Add( new KeyValuePair<string, IEntity>( "Attributes", attrib ) );
var value = new AttributeValueService( RockContext ).Queryable()
.Where( v => v.AttributeId == attrib.Id && v.EntityId == attributedEntity.Id )
.FirstOrDefault();
if ( value != null )
{
children.Add( new KeyValuePair<string, IEntity>( "AttributeValues", value ) );
}
}
}
//
// Allow for processors to adjust the list of children.
//
foreach ( var processor in FindEntityProcessors( GetEntityType( parentEntity ) ) )
{
processor.EvaluateChildEntities( parentEntity, children, this );
}
return children;
}
#endregion
}
}