-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathEntityDecoder.cs
352 lines (299 loc) · 14.6 KB
/
EntityDecoder.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
using System;
using System.Collections.Generic;
using System.Linq;
using Rock;
using Rock.Data;
using Rock.Model;
namespace EntityCoding
{
public class EntityDecoder : CodingHelper
{
#region Properties
/// <summary>
/// The map of original Guids to newly generated Guids.
/// </summary>
private Dictionary<Guid, Guid> GuidMap { get; set; }
/// <summary>
/// Contains the values provided by the user to be used during import.
/// </summary>
public Dictionary<string, IEntity> UserValues { 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 EntityDecoder( RockContext rockContext )
: base( rockContext )
{
GuidMap = new Dictionary<Guid, Guid>();
UserValues = new Dictionary<string, IEntity>();
}
#endregion
#region Public Methods
/// <summary>
/// Attempt to import the container of entities into the Rock database. Creates
/// a transaction inside the RockContext to perform all the entity creation so
/// if an error occurs everything will be left in a clean state.
/// </summary>
/// <param name="container">The container of all the encoded entities.</param>
/// <param name="dryRun">If true then we only attempt the import, nothing is actually saved.</param>
/// <param name="messages">Any messages, errors or otherwise, that should be displayed to the user.</param>
/// <returns>true if the import succeeded, false if it did not.</returns>
public bool Import( ExportedEntitiesContainer container, bool dryRun, out List<string> messages )
{
messages = new List<string>();
//
// Ensure we know about all referenced entity types.
//
var missingTypes = container.GetMissingEntityTypes();
if ( missingTypes.Any() )
{
messages.Add( string.Format( "The following EntityTypes are unknown and indicate you may be missing a plug-in: <ul><li>{0}</li></ul>", string.Join( "</li><li>", missingTypes ) ) );
return false;
}
//
// Generate a new Guid if we were asked to.
//
foreach ( var encodedEntity in container.Entities )
{
if ( encodedEntity.GenerateNewGuid )
{
MapNewGuid( encodedEntity.Guid );
}
}
using ( var transaction = RockContext.Database.BeginTransaction() )
{
try
{
//
// Walk each encoded entity and either verify an existing entity or
// create a new entity.
//
foreach ( var encodedEntity in container.Entities )
{
Type entityType = FindEntityType( encodedEntity.EntityType );
Guid entityGuid = FindMappedGuid( encodedEntity.Guid );
var entity = GetExistingEntity( encodedEntity.EntityType, entityGuid );
if ( entity == null )
{
try
{
entity = CreateNewEntity( encodedEntity );
}
catch ( Exception e )
{
throw new Exception( String.Format( "Error importing encoded entity: {0}", encodedEntity.ToJson() ), e );
}
messages.Add( string.Format( "Created: {0}, {1}", encodedEntity.EntityType, entityGuid ) );
}
else
{
messages.Add( string.Format( "Found Existing: {0}, {1}", encodedEntity.EntityType, entityGuid ) );
}
}
//
// Either commit the transaction or roll it back if we are doing a dry run.
//
if ( !dryRun )
{
transaction.Commit();
}
else
{
transaction.Rollback();
}
return true;
}
catch ( Exception e )
{
transaction.Rollback();
for ( Exception ex = e; ex != null; ex = ex.InnerException )
{
messages.Add( ex.Message + "\n" + ex.StackTrace );
}
return false;
}
}
}
/// <summary>
/// Gets the user defined value supplied to the decoder.
/// </summary>
/// <param name="key">The key that identifies the user value.</param>
/// <returns>An object that was provided by the user or null.</returns>
public object GetUserDefinedValue( string key )
{
if ( !UserValues.ContainsKey( key ) )
{
return null;
}
return UserValues[key];
}
/// <summary>
/// Finds and returns a Guid from the mapping dictionary. If no mapping
/// exists then the original Guid is returned.
/// </summary>
/// <param name="oldGuid">The original Guid value to map from.</param>
/// <returns>The Guid value that should be used, may be the same as oldGuid.</returns>
public Guid FindMappedGuid( Guid oldGuid )
{
return GuidMap.ContainsKey( oldGuid ) ? GuidMap[oldGuid] : oldGuid;
}
#endregion
#region Protected Methods
/// <summary>
/// Creates a new entity in the database from the encoded information. The entity
/// is saved before being returned.
/// </summary>
/// <param name="encodedEntity">The encoded entity information to create the new entity from.</param>
/// <returns>A reference to the new entity.</returns>
protected IEntity CreateNewEntity( EncodedEntity encodedEntity )
{
Type entityType = Reflection.FindType( typeof( IEntity ), encodedEntity.EntityType );
var service = Reflection.GetServiceForEntityType( entityType, RockContext );
if ( service != null )
{
var addMethod = service.GetType().GetMethod( "Add", new Type[] { entityType } );
if ( addMethod != null )
{
IEntity entity = ( IEntity ) Activator.CreateInstance( entityType );
RestoreEntityProperties( entity, encodedEntity );
entity.Guid = FindMappedGuid( encodedEntity.Guid );
//
// Do custom pre-save processing.
//
foreach ( var processor in FindEntityProcessors( entityType ) )
{
processor.ProcessImportedEntity( entity, encodedEntity, encodedEntity.GetTransformData( processor.Identifier.ToString() ), this );
}
//
// Special handling of AttributeQualifier because Guids may not be the same
// across installations and the AttributeId+Key columns make up a unique key.
//
if ( encodedEntity.EntityType == "Rock.Model.AttributeQualifier" )
{
var reference = encodedEntity.References.Where( r => r.Property == "AttributeId" ).First();
var attribute = GetExistingEntity( "Rock.Model.Attribute", FindMappedGuid( new Guid( ( string ) reference.Data ) ) );
string key = ( string ) encodedEntity.Properties["Key"];
var existingEntity = new AttributeQualifierService( RockContext )
.GetByAttributeId( attribute.Id )
.Where( a => a.Key == key )
.FirstOrDefault();
if ( existingEntity != null )
{
if ( entity.Guid != encodedEntity.Guid )
{
throw new Exception( "AttributeQualifier marked for new Guid but conflicting value already exists." );
}
GuidMap.AddOrReplace( encodedEntity.Guid, existingEntity.Guid );
return existingEntity;
}
}
//
// Special handling of Attribute's. The guid's might be different but if the entity type,
// entity qualifiers and key are the same, assume it's the same.
//
else if ( encodedEntity.EntityType == "Rock.Model.Attribute" )
{
var attribute = ( Rock.Model.Attribute ) entity;
var existingEntity = new AttributeService( RockContext )
.GetByEntityTypeId( attribute.EntityTypeId )
.Where( a => a.EntityTypeQualifierColumn == attribute.EntityTypeQualifierColumn && a.EntityTypeQualifierValue == attribute.EntityTypeQualifierValue && a.Key == attribute.Key )
.FirstOrDefault();
if ( existingEntity != null )
{
if ( entity.Guid != encodedEntity.Guid )
{
throw new Exception( "Attribute marked for new Guid but conflicting value already exists." );
}
GuidMap.AddOrReplace( encodedEntity.Guid, existingEntity.Guid );
return existingEntity;
}
}
//
// Special handling of AttributeValue's. The guid's might be different but if the attribute Id
// and entity Id are the same, assume it's the same.
//
else if ( encodedEntity.EntityType == "Rock.Model.AttributeValue" )
{
var attributeReference = encodedEntity.References.Where( r => r.Property == "AttributeId" ).First();
var attribute = GetExistingEntity( "Rock.Model.Attribute", FindMappedGuid( new Guid( ( string ) attributeReference.Data ) ) );
var entityReference = encodedEntity.References.Where( r => r.Property == "EntityId" ).First();
var entityRef = GetExistingEntity( entityReference.EntityType, FindMappedGuid( new Guid( ( string ) entityReference.Data ) ) );
var existingEntity = new AttributeValueService( RockContext )
.Queryable().Where( a => a.AttributeId == attribute.Id && a.EntityId == entityRef.Id )
.FirstOrDefault();
if ( existingEntity != null )
{
if ( entity.Guid != encodedEntity.Guid )
{
throw new Exception( "AttributeValue marked for new Guid but conflicting value already exists." );
}
GuidMap.AddOrReplace( encodedEntity.Guid, existingEntity.Guid );
return existingEntity;
}
}
addMethod.Invoke( service, new object[] { entity } );
RockContext.SaveChanges( true );
return entity;
}
}
throw new Exception( string.Format( "Failed to create new database entity for {0}_{1}", encodedEntity.EntityType, encodedEntity.Guid ) );
}
/// <summary>
/// Restore the property information from encodedEntity into the newly created entity.
/// </summary>
/// <param name="entity">The blank entity to be populated.</param>
/// <param name="encodedEntity">The encoded entity data.</param>
protected void RestoreEntityProperties( IEntity entity, EncodedEntity encodedEntity )
{
foreach ( var property in GetEntityProperties( entity ) )
{
//
// If this is a plain property, just set the value.
//
if ( encodedEntity.Properties.ContainsKey( property.Name ) )
{
var value = encodedEntity.Properties[property.Name];
//
// If this is a Guid, see if we need to remap it.
//
Guid? guidValue = null;
if ( value is Guid )
{
guidValue = ( Guid ) value;
value = FindMappedGuid( guidValue.Value );
}
else if ( value is string )
{
guidValue = ( ( string ) value ).AsGuidOrNull();
if ( guidValue.HasValue && guidValue.Value != FindMappedGuid( guidValue.Value ) )
{
value = FindMappedGuid( guidValue.Value ).ToString();
}
}
property.SetValue( entity, ChangeType( property.PropertyType, value ) );
}
}
//
// Restore all references.
//
foreach ( var reference in encodedEntity.References )
{
reference.Restore( entity, this );
}
}
/// <summary>
/// Creates a new map entry for the oldGuid. This generates a new Guid and
/// stores a reference between the two.
/// </summary>
/// <param name="oldGuid">The original Guid value to be mapped from.</param>
/// <returns>A new Guid value that should be used in place of oldGuid.</returns>
protected Guid MapNewGuid( Guid oldGuid )
{
GuidMap.Add( oldGuid, Guid.NewGuid() );
return GuidMap[oldGuid];
}
#endregion
}
}