in objectModel/CSharp/Microsoft.CommonDataModel.ObjectModel/Cdm/CdmEntityDefinition.cs [550:1104]
public async Task<CdmEntityDefinition> CreateResolvedEntityAsync(string newEntName, ResolveOptions resOpt = null, CdmFolderDefinition folder = null, string newDocName = null)
{
using (Logger.EnterScope(nameof(CdmEntityDefinition), Ctx, nameof(CreateResolvedEntityAsync)))
{
if (resOpt == null)
{
resOpt = new ResolveOptions(this, this.Ctx.Corpus.DefaultResolutionDirectives);
}
if (resOpt.WrtDoc == null)
{
Logger.Error((ResolveContext)this.Ctx, Tag, nameof(CreateResolvedEntityAsync), this.AtCorpusPath, CdmLogCode.ErrDocWrtDocNotfound);
return null;
}
if (string.IsNullOrEmpty(newEntName))
{
Logger.Error((ResolveContext)this.Ctx, Tag, nameof(CreateResolvedEntityAsync), this.AtCorpusPath, CdmLogCode.ErrResolveNewEntityNameNotSet);
return null;
}
// if the wrtDoc needs to be indexed (like it was just modified) then do that first
if (!await resOpt.WrtDoc.IndexIfNeeded(resOpt, true))
{
Logger.Error((ResolveContext)this.Ctx, Tag, nameof(CreateResolvedEntityAsync), this.AtCorpusPath, CdmLogCode.ErrIndexFailed);
return null;
}
if (folder == null)
{
folder = this.InDocument.Folder;
}
string fileName = (string.IsNullOrEmpty(newDocName)) ? $"{newEntName}.cdm.json" : newDocName;
string origDoc = this.InDocument.AtCorpusPath;
// Don't overwite the source document
string targetAtCorpusPath = $"{this.Ctx.Corpus.Storage.CreateAbsoluteCorpusPath(folder.AtCorpusPath, folder)}{fileName}";
if (StringUtils.EqualsWithIgnoreCase(targetAtCorpusPath, origDoc))
{
Logger.Error((ResolveContext)this.Ctx, Tag, nameof(CreateResolvedEntityAsync), this.AtCorpusPath, CdmLogCode.ErrDocEntityReplacementFailure, targetAtCorpusPath);
return null;
}
// make sure the corpus has a set of default artifact attributes
await this.Ctx.Corpus.PrepareArtifactAttributesAsync();
// make the top level attribute context for this entity
// for this whole section where we generate the attribute context tree and get resolved attributes
// set the flag that keeps all of the parent changes and document dirty from from happening
bool wasResolving = this.Ctx.Corpus.isCurrentlyResolving;
this.Ctx.Corpus.isCurrentlyResolving = true;
string entName = newEntName;
ResolveContext ctx = this.Ctx as ResolveContext;
CdmAttributeContext attCtxEnt = ctx.Corpus.MakeObject<CdmAttributeContext>(CdmObjectType.AttributeContextDef, entName, true);
attCtxEnt.Ctx = ctx;
attCtxEnt.InDocument = this.InDocument;
// cheating a bit to put the paths in the right place
AttributeContextParameters acp = new AttributeContextParameters
{
under = attCtxEnt,
type = CdmAttributeContextType.AttributeGroup,
Name = "attributeContext"
};
CdmAttributeContext attCtxAC = CdmAttributeContext.CreateChildUnder(resOpt, acp);
// this is the node that actually is first in the context we save. all definition refs should take the new perspective that they
// can only be understood through the resolvedFrom moniker
CdmEntityReference entRefThis = ctx.Corpus.MakeObject<CdmEntityReference>(CdmObjectType.EntityRef, this.GetName(), true);
entRefThis.Owner = this;
entRefThis.InDocument = this.InDocument; // need to set owner and inDocument to this starting entity so the ref will be portable to the new document
CdmObject prevOwner = this.Owner;
entRefThis.ExplicitReference = this;
// we don't want to change the owner of this entity to the entity reference
// re-assign whatever was there before
this.Owner = prevOwner;
AttributeContextParameters acpEnt = new AttributeContextParameters
{
under = attCtxAC,
type = CdmAttributeContextType.Entity,
Name = entName,
Regarding = entRefThis
};
// reset previous depth information in case there are left overs
resOpt.DepthInfo.Reset();
ResolveOptions resOptCopy = CdmAttributeContext.PrepareOptionsForResolveAttributes(resOpt);
// resolve attributes with this context. the end result is that each resolved attribute
// points to the level of the context where it was last modified, merged, created
var ras = this.FetchResolvedAttributes(resOptCopy, acpEnt);
if (resOptCopy.UsedResolutionGuidance)
{
Logger.Warning(ctx, Tag, nameof(CreateResolvedEntityAsync), this.AtCorpusPath, CdmLogCode.WarnDeprecatedResolutionGuidance);
}
if (ras == null)
{
return null;
}
this.Ctx.Corpus.isCurrentlyResolving = wasResolving;
// make a new document in given folder if provided or the same folder as the source entity
folder.Documents.Remove(fileName);
CdmDocumentDefinition docRes = folder.Documents.Add(fileName);
// add a import of the source document
origDoc = this.Ctx.Corpus.Storage.CreateRelativeCorpusPath(origDoc, docRes); // just in case we missed the prefix
docRes.Imports.Add(origDoc, "resolvedFrom");
docRes.DocumentVersion = this.InDocument.DocumentVersion;
// make the empty entity
CdmEntityDefinition entResolved = docRes.Definitions.Add(entName);
// grab that context that comes from fetchResolvedAttributes. We are sure this tree is a copy that we can own, so no need to copy it again
CdmAttributeContext attCtx = null;
if (attCtxAC != null && attCtxAC.Contents != null && attCtxAC.Contents.Count == 1)
{
attCtx = attCtxAC.Contents[0] as CdmAttributeContext;
}
entResolved.AttributeContext = attCtx;
if (attCtx != null)
{
// fix all of the definition references, parent and lineage references, owner documents, etc. in the context tree
attCtx.FinalizeAttributeContext(resOptCopy, $"{entName}/attributeContext/", docRes, this.InDocument, "resolvedFrom", true);
// TEST CODE
// run over the resolved attributes and make sure none have the dummy context
//Action<ResolvedAttributeSet> testResolveAttributeCtx = null;
//testResolveAttributeCtx = (rasSub) =>
//{
// if (rasSub.Set.Count != 0 && rasSub.AttributeContext.AtCorpusPath.StartsWith("cacheHolder"))
// System.Diagnostics.Debug.WriteLine("Bad");
// rasSub.Set.ForEach(ra =>
// {
// if (ra.AttCtx.AtCorpusPath.StartsWith("cacheHolder"))
// System.Diagnostics.Debug.WriteLine("Bad");
// // the target for a resolved att can be a typeAttribute OR it can be another resolvedAttributeSet (meaning a group)
// if (ra.Target is ResolvedAttributeSet)
// {
// testResolveAttributeCtx(ra.Target as ResolvedAttributeSet);
// }
// });
//};
//testResolveAttributeCtx(ras);
}
// add the traits of the entity, also add to attribute context top node
ResolvedTraitSet rtsEnt = this.FetchResolvedTraits(resOpt);
rtsEnt.Set.ForEach(rt =>
{
var traitRef = CdmObjectBase.ResolvedTraitToTraitRef(resOptCopy, rt);
(entResolved as CdmObjectDefinition).ExhibitsTraits.Add(traitRef);
traitRef = CdmObjectBase.ResolvedTraitToTraitRef(resOptCopy, rt); // fresh copy
entResolved.AttributeContext?.ExhibitsTraits.Add(traitRef);
});
// special trait to explain this is a resolved entity
entResolved.IndicateAbstractionLevel("resolved", resOpt);
if (entResolved.AttributeContext != null)
{
// the attributes have been named, shaped, etc for this entity so now it is safe to go and
// make each attribute context level point at these final versions of attributes
// what we have is a resolved attribute set (maybe with structure) where each ra points at the best tree node
// we also have the tree of context, we need to flip this around so that the right tree nodes point at the paths to the
// right attributes. so run over the resolved atts and then add a path reference to each one into the context contents where is last lived
IDictionary<string, int> attPath2Order = new Dictionary<string, int>();
HashSet<string> finishedGroups = new HashSet<string>();
HashSet<CdmAttributeContext> allPrimaryCtx = new HashSet<CdmAttributeContext>(); // keep a list so it is easier to think about these later
Action<ResolvedAttributeSet, string> pointContextAtResolvedAtts = null;
pointContextAtResolvedAtts = (rasSub, path) =>
{
rasSub.Set.ForEach(ra =>
{
var raCtx = ra.AttCtx;
var refs = raCtx.Contents;
allPrimaryCtx.Add(raCtx);
string attRefPath = path + ra.ResolvedName;
// the target for a resolved att can be a typeAttribute OR it can be another resolvedAttributeSet (meaning a group)
if (ra.Target is CdmAttribute)
{
// it was an attribute, add to the content of the context, also, keep track of the ordering for all of the att paths we make up
// as we go through the resolved attributes, this is the order of atts in the final resolved entity
if (!attPath2Order.ContainsKey(attRefPath))
{
var attRef = this.Ctx.Corpus.MakeObject<CdmObjectReferenceBase>(CdmObjectType.AttributeRef, attRefPath, true);
// only need one explanation for this path to the insert order
attPath2Order.Add(attRef.NamedReference, ra.InsertOrder);
raCtx.Contents.Add(attRef);
}
}
else
{
// a group, so we know an attribute group will get created later with the name of the group and the things it contains will be in
// the members of that group
attRefPath += "/members/";
if (!finishedGroups.Contains(attRefPath))
{
pointContextAtResolvedAtts(ra.Target as ResolvedAttributeSet, attRefPath);
finishedGroups.Add(attRefPath);
}
}
});
};
pointContextAtResolvedAtts(ras, entName + "/hasAttributes/");
// the generated attribute structures sometimes has a LOT of extra nodes that don't say anything or explain anything
// our goal now is to prune that tree to just have the stuff one may need
// do this by keeping the leafs and all of the lineage nodes for the attributes that end up in the resolved entity
// along with some special nodes that explain entity structure and inherit
//System.Diagnostics.Debug.Write($"res ent {entName}");
if (!attCtx.PruneToScope(allPrimaryCtx))
{
// TODO: log error
return null;
}
// create an all-up ordering of attributes at the leaves of this tree based on insert order
// sort the attributes in each context by their creation order and mix that with the other sub-contexts that have been sorted
Func<CdmAttributeContext, int?> orderContents = null;
Func<CdmObject, int?> getOrderNum = (item) =>
{
if (item.ObjectType == CdmObjectType.AttributeContextDef && orderContents != null)
{
return orderContents(item as CdmAttributeContext);
}
else if (item.ObjectType == CdmObjectType.AttributeRef)
{
string attName = (item as CdmAttributeReference).NamedReference;
int o = attPath2Order[attName];
return o;
}
else
{
return -1; // put the mystery item on top.
}
};
orderContents = (CdmAttributeContext under) =>
{
if (under.LowestOrder == null)
{
under.LowestOrder = -1; // used for group with nothing but traits
if (under.Contents.Count == 1)
{
under.LowestOrder = getOrderNum(under.Contents[0]);
}
else
{
under.Contents.AllItems.Sort((l, r) =>
{
int lNum = -1;
int rNum = -1;
int? aux;
aux = getOrderNum(l);
if (aux != null)
{
lNum = (int)aux;
}
aux = getOrderNum(r);
if (aux != null)
{
rNum = (int)aux;
}
if (lNum != -1 && (under.LowestOrder == -1 || lNum < under.LowestOrder))
under.LowestOrder = lNum;
if (rNum != -1 && (under.LowestOrder == -1 || rNum < under.LowestOrder))
under.LowestOrder = rNum;
return lNum - rNum;
});
}
}
return under.LowestOrder;
};
orderContents(attCtx);
// resolved attributes can gain traits that are applied to an entity when referenced
// since these traits are described in the context, it is redundant and messy to list them in the attribute
// so, remove them. create and cache a set of names to look for per context
// there is actually a hierarchy to all attributes from the base entity should have all traits applied independently of the
// sub-context they come from. Same is true of attribute entities. so do this recursively top down
var ctx2traitNames = new Dictionary<CdmAttributeContext, HashSet<string>>();
Action<CdmAttributeContext, HashSet<string>> collectContextTraits = null;
collectContextTraits = (subAttCtx, inheritedTraitNames) =>
{
var traitNamesHere = new HashSet<string>(inheritedTraitNames);
var traitsHere = subAttCtx.ExhibitsTraits;
if (traitsHere != null)
foreach (var tat in traitsHere) { traitNamesHere.Add(tat.NamedReference); }
ctx2traitNames.Add(subAttCtx, traitNamesHere);
subAttCtx.Contents.AllItems.ForEach((cr) =>
{
if (cr.ObjectType == CdmObjectType.AttributeContextDef)
{
// do this for all types?
collectContextTraits(cr as CdmAttributeContext, traitNamesHere);
}
});
};
collectContextTraits(attCtx, new HashSet<string>());
// add the attributes, put them in attribute groups if structure needed
IDictionary<ResolvedAttribute, string> resAtt2RefPath = new Dictionary<ResolvedAttribute, string>();
Action<ResolvedAttributeSet, dynamic, string> addAttributes = null;
addAttributes = (rasSub, container, path) =>
{
rasSub.Set.ForEach(ra =>
{
string attPath = path + ra.ResolvedName;
// use the path of the context associated with this attribute to find the new context that matches on path
CdmAttributeContext raCtx = ra.AttCtx;
if (ra.Target is ResolvedAttributeSet)
{
// this is a set of attributes.
// make an attribute group to hold them
CdmAttributeGroupDefinition attGrp = this.Ctx.Corpus.MakeObject<CdmAttributeGroupDefinition>(CdmObjectType.AttributeGroupDef, ra.ResolvedName, false);
attGrp.AttributeContext = this.Ctx.Corpus.MakeObject<CdmAttributeContextReference>(CdmObjectType.AttributeContextRef, raCtx.AtCorpusPath, true);
// debugLineage
//attGrp.AttributeContext.NamedReference = $"{ raCtx.AtCoprusPath}({raCtx.Id})";
// take any traits from the set and make them look like traits exhibited by the group
HashSet<string> avoidSet = ctx2traitNames[raCtx];
// traits with the same name can show up on entities and attributes AND have different meanings.
avoidSet.Clear();
ResolvedTraitSet rtsAtt = ra.ResolvedTraits;
rtsAtt.Set.ForEach(rt =>
{
if (rt.Trait.Ugly != true)
{
// don't mention your ugly traits
if (avoidSet?.Contains(rt.TraitName) != true)
{
// avoid the ones from the context
var traitRef = CdmObjectBase.ResolvedTraitToTraitRef(resOptCopy, rt);
(attGrp as CdmObjectDefinitionBase).ExhibitsTraits.Add(traitRef);
}
}
});
// wrap it in a reference and then recurse with this as the new container
CdmAttributeGroupReference attGrpRef = this.Ctx.Corpus.MakeObject<CdmAttributeGroupReference>(CdmObjectType.AttributeGroupRef, null, false);
attGrpRef.ExplicitReference = attGrp;
container.AddAttributeDef(attGrpRef);
// isn't this where ...
addAttributes(ra.Target as ResolvedAttributeSet, attGrp, attPath + "/members/");
}
else
{
CdmTypeAttributeDefinition att = this.Ctx.Corpus.MakeObject<CdmTypeAttributeDefinition>(CdmObjectType.TypeAttributeDef, ra.ResolvedName, false);
att.AttributeContext = this.Ctx.Corpus.MakeObject<CdmAttributeContextReference>(CdmObjectType.AttributeContextRef, raCtx.AtCorpusPath, true);
// debugLineage
//att.AttributeContext.NamedReference = $"{ raCtx.AtCoprusPath}({raCtx.Id})";
HashSet<string> avoidSet = ctx2traitNames[raCtx];
// i don't know why i thought this was the right thing to do,
// traits with the same name can show up on entities and attributes AND have different meanings.
avoidSet.Clear();
// i really need to figure out the effects of this before making this change
// without it, some traits don't make it to the resolved entity
// with it, too many traits might make it there
ResolvedTraitSet rtsAtt = ra.ResolvedTraits;
rtsAtt.Set.ForEach(rt =>
{
if (rt.Trait.Ugly != true)
{ // don't mention your ugly traits
if (avoidSet?.Contains(rt.TraitName) != true)
{ // avoid the ones from the context
var traitRef = CdmObjectBase.ResolvedTraitToTraitRef(resOptCopy, rt);
((CdmTypeAttributeDefinition)att).AppliedTraits.Add(traitRef);
// the trait that points at other entities for foreign keys, that is trouble
// the string value in the table needs to be a relative path from the document of this entity
// to the document of the related entity. but, right now it is a relative path to the source entity
// so find those traits, and adjust the paths in the tables they hold
if (rt.TraitName == "is.linkedEntity.identifier")
{
// grab the content of the table from the new ref (should be a copy of orig)
List<List<string>> linkTable = null;
if (traitRef.Arguments != null && traitRef.Arguments.Count > 0)
{
linkTable = ((traitRef.Arguments?[0].Value as CdmEntityReference)?
.ExplicitReference as CdmConstantEntityDefinition)?
.ConstantValues;
}
if (linkTable != null && linkTable.Count > 0)
{
foreach (var row in linkTable)
{
if (row.Count == 2 || row.Count == 3) // either the old table or the new one with relationship name can be there
{
// entity path an attribute name
string fixedPath = row[0];
fixedPath = this.Ctx.Corpus.Storage.CreateAbsoluteCorpusPath(fixedPath, this.InDocument); // absolute from relative to this
fixedPath = this.Ctx.Corpus.Storage.CreateRelativeCorpusPath(fixedPath, docRes); // realtive to new entity
row[0] = fixedPath;
}
}
}
}
}
}
});
// none of the dataformat traits have the bit set that will make them turn into a property
// this is intentional so that the format traits make it into the resolved object
// but, we still want a guess as the data format, so get it and set it.
var impliedDataFormat = att.DataFormat;
if (impliedDataFormat != CdmDataFormat.Unknown)
att.DataFormat = impliedDataFormat;
container.AddAttributeDef(att);
resAtt2RefPath[ra] = attPath;
}
});
};
addAttributes(ras, entResolved, entName + "/hasAttributes/");
// any resolved traits that hold arguments with attribute refs should get 'fixed' here
Action<CdmTraitReference, string, bool> replaceTraitAttRef = (tr, entityHint, isAttributeContext) =>
{
if (tr.Arguments != null)
{
foreach (CdmArgumentDefinition arg in tr.Arguments.AllItems)
{
dynamic v = arg.UnResolvedValue != null ? arg.UnResolvedValue : arg.Value;
// is this an attribute reference?
if (v != null && (v as CdmObject)?.ObjectType == CdmObjectType.AttributeRef)
{
// only try this if the reference has no path to it (only happens with intra-entity att refs)
var attRef = v as CdmAttributeReference;
if (!string.IsNullOrEmpty(attRef.NamedReference) && attRef.NamedReference.IndexOf('/') == -1)
{
if (arg.UnResolvedValue == null)
arg.UnResolvedValue = arg.Value;
// give a promise that can be worked out later. assumption is that the attribute must come from this entity.
var newAttRef = this.Ctx.Corpus.MakeRef<CdmAttributeReference>(CdmObjectType.AttributeRef, entityHint + "/(resolvedAttributes)/" + attRef.NamedReference, true);
// inDocument is not propagated during resolution, so set it here
newAttRef.InDocument = arg.InDocument;
arg.Value = newAttRef;
}
}
}
}
};
// fix entity traits
if (entResolved.ExhibitsTraits != null)
foreach (var et in entResolved.ExhibitsTraits)
{
if (et is CdmTraitReference)
{
replaceTraitAttRef(et as CdmTraitReference, newEntName, false);
}
}
// fix context traits
Action<CdmAttributeContext, string> fixContextTraits = null;
fixContextTraits = (subAttCtx, entityHint) =>
{
var traitsHere = subAttCtx.ExhibitsTraits;
if (traitsHere != null)
{
foreach (var tr in traitsHere)
{
if (tr is CdmTraitReference)
{
replaceTraitAttRef(tr as CdmTraitReference, entityHint, true);
}
}
}
subAttCtx.Contents.AllItems.ForEach((cr) =>
{
if (cr.ObjectType == CdmObjectType.AttributeContextDef)
{
// if this is a new entity context, get the name to pass along
CdmAttributeContext subSubAttCtx = (CdmAttributeContext)cr;
string subEntityHint = entityHint;
if (subSubAttCtx.Type == CdmAttributeContextType.Entity && subSubAttCtx.Definition != null)
{
subEntityHint = subSubAttCtx.Definition.NamedReference;
}
// do this for all types
fixContextTraits(subSubAttCtx, subEntityHint);
}
});
};
fixContextTraits(attCtx, newEntName);
// and the attribute traits
var entAttributes = entResolved.Attributes;
if (entAttributes != null)
{
foreach (var entAtt in entAttributes)
{
CdmTraitCollection attTraits = entAtt.AppliedTraits;
if (attTraits != null)
{
foreach (var tr in attTraits)
{
if (tr is CdmTraitReference)
{
replaceTraitAttRef(tr as CdmTraitReference, newEntName, false);
}
}
}
}
}
}
// we are about to put this content created in the context of various documents (like references to attributes from base entities, etc.)
// into one specific document. all of the borrowed refs need to work. so, re-write all string references to work from this new document
// the catch-22 is that the new document needs these fixes done before it can be used to make these fixes.
// the fix needs to happen in the middle of the refresh
// trigger the document to refresh current content into the resolved OM
if (attCtx != null)
attCtx.Parent = null; // remove the fake parent that made the paths work
ResolveOptions resOptNew = resOpt.Copy();
resOptNew.LocalizeReferencesFor = docRes;
resOptNew.WrtDoc = docRes;
if (!await docRes.RefreshAsync(resOptNew))
{
Logger.Error((ResolveContext)this.Ctx, Tag, nameof(CreateResolvedEntityAsync), this.AtCorpusPath, CdmLogCode.ErrIndexFailed);
return null;
}
// get a fresh ref
entResolved = docRes.FetchObjectFromDocumentPath(entName, resOptNew) as CdmEntityDefinition;
this.Ctx.Corpus.resEntMap[this.AtCorpusPath] = entResolved.AtCorpusPath;
return entResolved;
}
}