Example #1
0
        /// <inheritdoc />
        public override CdmObject Copy(ResolveOptions resOpt = null, CdmObject host = null)
        {
            // since we need to call the base copy which will only return a document when there is no host, make a fake host here
            CdmManifestDefinition tempHost = host as CdmManifestDefinition;

            if (tempHost == null)
            {
                tempHost = new CdmManifestDefinition(this.Ctx, this.ManifestName);
            }

            CdmManifestDefinition copy = base.Copy(resOpt, tempHost) as CdmManifestDefinition;

            copy.ManifestName              = this.ManifestName;
            copy.Explanation               = this.Explanation;
            copy.LastFileStatusCheckTime   = this.LastFileStatusCheckTime;
            copy.LastFileModifiedTime      = this.LastFileModifiedTime;
            copy.LastChildFileModifiedTime = this.LastChildFileModifiedTime;

            copy.Entities.Clear();
            foreach (var ent in this.Entities)
            {
                copy.Entities.Add(ent.Copy(resOpt) as CdmEntityDeclarationDefinition);
            }
            copy.Relationships.Clear();
            foreach (var rel in this.Relationships)
            {
                copy.Relationships.Add(rel.Copy(resOpt) as CdmE2ERelationship);
            }
            copy.SubManifests.Clear();
            foreach (var man in this.SubManifests)
            {
                copy.SubManifests.Add(man.Copy(resOpt) as CdmManifestDeclarationDefinition);
            }
            copy.ExhibitsTraits.Clear();
            foreach (var et in this.ExhibitsTraits)
            {
                copy.ExhibitsTraits.Add(et.Copy() as CdmTraitReferenceBase);
            }

            return(copy);
        }
Example #2
0
        /// <summary>
        /// finds and returns an entity object from an EntityDeclaration object that probably comes from a manifest
        /// </summary>
        /// <param name="entity"></param>
        /// <param name="manifest"></param>
        /// <returns></returns>
        internal async Task <CdmEntityDefinition> GetEntityFromReference(CdmEntityDeclarationDefinition entity, CdmManifestDefinition manifest)
        {
            string entityPath = await this.GetEntityPathFromDeclaration(entity, manifest);

            CdmEntityDefinition result = await this.Ctx.Corpus.FetchObjectAsync <CdmEntityDefinition>(entityPath);

            if (result == null)
            {
                Logger.Error(this.Ctx, Tag, nameof(GetEntityFromReference), this.AtCorpusPath, CdmLogCode.ErrResolveEntityFailure, entityPath);
            }

            return(result);
        }
Example #3
0
        /// <summary>
        /// Creates a resolved copy of the manifest.
        /// newEntityDocumentNameFormat specifies a pattern to use when creating documents for resolved entites.
        /// The default is "{f}resolved/{n}.cdm.json" to avoid a document name conflict with documents in the same folder as the manifest.
        /// Every instance of the string {n} is replaced with the entity name from the source manifest.
        /// Every instance of the string {f} is replaced with the folder path from the source manifest to the source entity
        /// (if there is one that is possible as a relative location, else nothing).
        /// </summary>
        /// <param name="newManifestName"></param>
        /// <param name="newEntityDocumentNameFormat"></param>
        /// <param name="Directives"></param>
        /// <returns></returns>
        public async Task <CdmManifestDefinition> CreateResolvedManifestAsync(string newManifestName, string newEntityDocumentNameFormat, AttributeResolutionDirectiveSet Directives = null)
        {
            using (Logger.EnterScope(nameof(CdmManifestDefinition), Ctx, nameof(CreateResolvedManifestAsync)))
            {
                if (this.Entities == null)
                {
                    return(null);
                }
                if (this.Folder == null)
                {
                    Logger.Error(this.Ctx as ResolveContext, Tag, nameof(CreateResolvedManifestAsync), this.AtCorpusPath, CdmLogCode.ErrResolveManifestFailed, this.ManifestName);
                    return(null);
                }

                if (newEntityDocumentNameFormat == null)
                {
                    newEntityDocumentNameFormat = "{f}resolved/{n}.cdm.json";
                }
                else if (newEntityDocumentNameFormat == "") // for back compat
                {
                    newEntityDocumentNameFormat = "{n}.cdm.json";
                }
                else if (!newEntityDocumentNameFormat.Contains("{n}")) // for back compat
                {
                    newEntityDocumentNameFormat = newEntityDocumentNameFormat + "/{n}.cdm.json";
                }

                string sourceManifestPath       = this.Ctx.Corpus.Storage.CreateAbsoluteCorpusPath(this.AtCorpusPath, this);
                string sourceManifestFolderPath = this.Ctx.Corpus.Storage.CreateAbsoluteCorpusPath(this.Folder.AtCorpusPath, this);

                int resolvedManifestPathSplit = newManifestName.LastIndexOf("/") + 1;
                CdmFolderDefinition resolvedManifestFolder;
                if (resolvedManifestPathSplit > 0)
                {
                    var resolvedManifestPath = newManifestName.Substring(0, resolvedManifestPathSplit);
                    var newFolderPath        = this.Ctx.Corpus.Storage.CreateAbsoluteCorpusPath(resolvedManifestPath, this);
                    resolvedManifestFolder = await this.Ctx.Corpus.FetchObjectAsync <CdmFolderDefinition>(newFolderPath);

                    if (resolvedManifestFolder == null)
                    {
                        Logger.Error(this.Ctx as ResolveContext, Tag, nameof(CreateResolvedManifestAsync), this.AtCorpusPath, CdmLogCode.ErrResolveFolderNotFound, newFolderPath);
                        return(null);
                    }
                    newManifestName = newManifestName.Substring(resolvedManifestPathSplit);
                }
                else
                {
                    resolvedManifestFolder = this.Owner as CdmFolderDefinition;
                }

                Logger.Debug(this.Ctx as ResolveContext, Tag, nameof(CreateResolvedManifestAsync), this.AtCorpusPath, $"resolving manifest {sourceManifestPath}");

                // Using the references present in the resolved entities, get an entity
                // create an imports doc with all the necessary resolved entity references and then resolve it
                // sometimes they might send the docname, that makes sense a bit, don't include the suffix in the name
                if (newManifestName.ToLowerInvariant().EndsWith(".manifest.cdm.json"))
                {
                    newManifestName = newManifestName.Substring(0, newManifestName.Length - ".manifest.cdm.json".Length);
                }
                var resolvedManifest = new CdmManifestDefinition(this.Ctx, newManifestName);

                // bring over any imports in this document or other bobbles
                resolvedManifest.Schema          = this.Schema;
                resolvedManifest.Explanation     = this.Explanation;
                resolvedManifest.DocumentVersion = this.DocumentVersion;
                foreach (CdmImport imp in this.Imports)
                {
                    resolvedManifest.Imports.Add((CdmImport)imp.Copy());
                }

                // add the new document to the folder
                if (resolvedManifestFolder.Documents.Add(resolvedManifest) == null)
                {
                    // when would this happen?
                    return(null);
                }

                foreach (var entity in this.Entities)
                {
                    var entDef = await this.GetEntityFromReference(entity, this);

                    if (entDef == null)
                    {
                        Logger.Error(this.Ctx as ResolveContext, Tag, nameof(CreateResolvedManifestAsync), this.AtCorpusPath, CdmLogCode.ErrResolveEntityRefError);
                        return(null);
                    }

                    if (entDef.InDocument.Folder == null)
                    {
                        Logger.Error(this.Ctx as ResolveContext, Tag, nameof(CreateResolvedManifestAsync), this.AtCorpusPath, CdmLogCode.ErrDocIsNotFolder, entDef.EntityName);
                        return(null);
                    }
                    // get the path from this manifest to the source entity. this will be the {f} replacement value
                    string sourceEntityFullPath = this.Ctx.Corpus.Storage.CreateAbsoluteCorpusPath(entDef.InDocument.Folder.AtCorpusPath, this);
                    string f = "";
                    if (sourceEntityFullPath.StartsWith(sourceManifestFolderPath))
                    {
                        f = sourceEntityFullPath.Substring(sourceManifestFolderPath.Length);
                    }

                    string newDocumentFullPath = newEntityDocumentNameFormat.Replace("{n}", entDef.EntityName);

                    newDocumentFullPath = newDocumentFullPath.Replace("{f}", f);
                    newDocumentFullPath = this.Ctx.Corpus.Storage.CreateAbsoluteCorpusPath(newDocumentFullPath, this);

                    int    newDocumentPathSplit = newDocumentFullPath.LastIndexOf("/") + 1;
                    string newDocumentPath      = newDocumentFullPath.Substring(0, newDocumentPathSplit);
                    string newDocumentName      = newDocumentFullPath.Substring(newDocumentPathSplit);

                    // make sure the new folder exists
                    var folder = await this.Ctx.Corpus.FetchObjectAsync <CdmFolderDefinition>(newDocumentPath) as CdmFolderDefinition;

                    if (folder == null)
                    {
                        Logger.Error(this.Ctx as ResolveContext, Tag, nameof(CreateResolvedManifestAsync), this.AtCorpusPath, CdmLogCode.ErrResolveFolderNotFound, newDocumentPath);
                        return(null);
                    }

                    // Next create the resolved entity
                    AttributeResolutionDirectiveSet withDirectives = Directives != null ? Directives : this.Ctx.Corpus.DefaultResolutionDirectives;
                    var resOpt = new ResolveOptions
                    {
                        WrtDoc     = entDef.InDocument,
                        Directives = withDirectives?.Copy()
                    };

                    Logger.Debug(this.Ctx as ResolveContext, Tag, nameof(CreateResolvedManifestAsync), this.AtCorpusPath, $"resolving entity {sourceEntityFullPath} to document {newDocumentFullPath}");

                    var resolvedEntity = await entDef.CreateResolvedEntityAsync(entDef.EntityName, resOpt, folder, newDocumentName);

                    if (resolvedEntity == null)
                    {
                        // Fail all resolution, if any one entity resolution fails
                        return(null);
                    }

                    var result = entity.Copy(resOpt) as CdmEntityDeclarationDefinition;
                    if (result.ObjectType == CdmObjectType.LocalEntityDeclarationDef)
                    {
                        result.EntityPath = this.Ctx.Corpus.Storage.CreateRelativeCorpusPath(resolvedEntity.AtCorpusPath, resolvedManifest) ?? result.AtCorpusPath;
                    }

                    resolvedManifest.Entities.Add(result);
                }

                Logger.Debug(this.Ctx as ResolveContext, Tag, nameof(CreateResolvedManifestAsync), this.AtCorpusPath, "calculating relationships");

                // calculate the entity graph for just this manifest and any submanifests
                await this.Ctx.Corpus.CalculateEntityGraphAsync(resolvedManifest);

                // stick results into the relationships list for the manifest
                // only put in relationships that are between the entities that are used in the manifest
                await resolvedManifest.PopulateManifestRelationshipsAsync(CdmRelationshipDiscoveryStyle.Exclusive);

                // needed until Matt's changes with collections where I can propigate
                resolvedManifest.IsDirty = true;
                return(resolvedManifest);
            }
        }
        // finds and returns an entity object from an EntityDeclaration object that probably comes from a manifest
        internal async Task <CdmEntityDefinition> GetEntityFromReference(CdmEntityDeclarationDefinition entity, CdmManifestDefinition manifest)
        {
            string entityPath = await this.GetEntityPathFromDeclaration(entity, manifest);

            CdmEntityDefinition result = await this.Ctx.Corpus.FetchObjectAsync <CdmEntityDefinition>(entityPath);

            if (result == null)
            {
                Logger.Error(nameof(CdmManifestDefinition), this.Ctx, $"failed to resolve entity {entityPath}", "GetEntityFromReference");
            }

            return(result);
        }
        /// <summary>
        /// Populates the relationships that the entities in the current manifest are involved in.
        /// </summary>
        public async Task PopulateManifestRelationshipsAsync(CdmRelationshipDiscoveryStyle option = CdmRelationshipDiscoveryStyle.All)
        {
            this.Relationships.Clear();
            HashSet <string> relCache = new HashSet <string>();

            if (this.Entities != null)
            {
                foreach (CdmEntityDeclarationDefinition entDec in this.Entities)
                {
                    string entPath = await this.GetEntityPathFromDeclaration(entDec, this);

                    CdmEntityDefinition currEntity = await this.Ctx.Corpus.FetchObjectAsync <CdmEntityDefinition>(entPath);

                    if (currEntity == null)
                    {
                        continue;
                    }

                    // handle the outgoing relationships
                    List <CdmE2ERelationship> outgoingRels = this.Ctx.Corpus.FetchOutgoingRelationships(currEntity);
                    if (outgoingRels != null)
                    {
                        foreach (CdmE2ERelationship rel in outgoingRels)
                        {
                            string cacheKey = rel2CacheKey(rel);
                            if (!relCache.Contains(cacheKey) && this.IsRelAllowed(rel, option))
                            {
                                this.Relationships.Add(this.LocalizeRelToManifest(rel));
                                relCache.Add(cacheKey);
                            }
                        }
                    }

                    List <CdmE2ERelationship> incomingRels = this.Ctx.Corpus.FetchIncomingRelationships(currEntity);

                    if (incomingRels != null)
                    {
                        foreach (CdmE2ERelationship inRel in incomingRels)
                        {
                            // get entity object for current toEntity
                            CdmEntityDefinition currentInBase = await this.Ctx.Corpus.FetchObjectAsync <CdmEntityDefinition>(inRel.ToEntity, this);

                            if (currentInBase == null)
                            {
                                continue;
                            }

                            // create graph of inheritance for to currentInBase
                            // graph represented by an array where entity at i extends entity at i+1
                            List <CdmEntityDefinition> toInheritanceGraph = new List <CdmEntityDefinition>();
                            while (currentInBase != null)
                            {
                                var resOpt = new ResolveOptions
                                {
                                    WrtDoc = currentInBase.InDocument
                                };
                                currentInBase = currentInBase.ExtendsEntity?.FetchObjectDefinition <CdmEntityDefinition>(resOpt);
                                if (currentInBase != null)
                                {
                                    toInheritanceGraph.Add(currentInBase);
                                }
                            }

                            // add current incoming relationship
                            string cacheKey = rel2CacheKey(inRel);
                            if (!relCache.Contains(cacheKey) && this.IsRelAllowed(inRel, option))
                            {
                                this.Relationships.Add(this.LocalizeRelToManifest(inRel));
                                relCache.Add(cacheKey);
                            }

                            // if A points at B, A's base classes must point at B as well
                            foreach (CdmEntityDefinition baseEntity in toInheritanceGraph)
                            {
                                List <CdmE2ERelationship> incomingRelsForBase = this.Ctx.Corpus.FetchIncomingRelationships(baseEntity);

                                if (incomingRelsForBase != null)
                                {
                                    foreach (CdmE2ERelationship inRelBase in incomingRelsForBase)
                                    {
                                        CdmE2ERelationship newRel = new CdmE2ERelationship(this.Ctx, "")
                                        {
                                            FromEntity          = inRelBase.FromEntity,
                                            FromEntityAttribute = inRelBase.FromEntityAttribute,
                                            ToEntity            = inRel.ToEntity,
                                            ToEntityAttribute   = inRel.ToEntityAttribute
                                        };

                                        string baseRelCacheKey = rel2CacheKey(newRel);
                                        if (!relCache.Contains(baseRelCacheKey) && this.IsRelAllowed(newRel, option))
                                        {
                                            this.Relationships.Add(this.LocalizeRelToManifest(newRel));
                                            relCache.Add(baseRelCacheKey);
                                        }
                                    }
                                }
                            }
                        }
                    }
                }

                if (this.SubManifests != null)
                {
                    foreach (CdmManifestDeclarationDefinition subManifestDef in this.SubManifests)
                    {
                        string corpusPath = this.Ctx.Corpus.Storage.CreateAbsoluteCorpusPath(subManifestDef.Definition, this);
                        CdmManifestDefinition subManifest = await this.Ctx.Corpus.FetchObjectAsync <CdmManifestDefinition>(corpusPath);

                        await(subManifest as CdmManifestDefinition).PopulateManifestRelationshipsAsync(option);
                    }
                }
            }
        }
        /// Creates a resolved copy of the manifest.
        /// newEntityDocumentNameFormat specifies a pattern to use when creating documents for resolved entites.
        /// The default is "{f}resolved/{n}.cdm.json" to avoid a document name conflict with documents in the same folder as the manifest.
        /// Every instance of the string {n} is replaced with the entity name from the source manifest.
        /// Every instance of the string {f} is replaced with the folder path from the source manifest to the source entity
        /// (if there is one that is possible as a relative location, else nothing).
        public async Task <CdmManifestDefinition> CreateResolvedManifestAsync(string newManifestName, string newEntityDocumentNameFormat)
        {
            if (this.Entities == null)
            {
                return(null);
            }

            if (newEntityDocumentNameFormat == null)
            {
                newEntityDocumentNameFormat = "{f}resolved/{n}.cdm.json";
            }
            else if (newEntityDocumentNameFormat == "") // for back compat
            {
                newEntityDocumentNameFormat = "{n}.cdm.json";
            }
            else if (!newEntityDocumentNameFormat.Contains("{n}")) // for back compat
            {
                newEntityDocumentNameFormat = newEntityDocumentNameFormat + "/{n}.cdm.json";
            }

            string sourceManifestPath       = this.Ctx.Corpus.Storage.CreateAbsoluteCorpusPath(this.AtCorpusPath, this);
            string sourceManifestFolderPath = this.Ctx.Corpus.Storage.CreateAbsoluteCorpusPath(this.Folder.AtCorpusPath, this);

            int resolvedManifestPathSplit = newManifestName.LastIndexOf("/") + 1;
            CdmFolderDefinition resolvedManifestFolder;

            if (resolvedManifestPathSplit > 0)
            {
                var resolvedManifestPath = newManifestName.Substring(0, resolvedManifestPathSplit);
                var newFolderPath        = this.Ctx.Corpus.Storage.CreateAbsoluteCorpusPath(resolvedManifestPath, this);
                resolvedManifestFolder = await this.Ctx.Corpus.FetchObjectAsync <CdmFolderDefinition>(newFolderPath);

                if (resolvedManifestFolder == null)
                {
                    Logger.Error(nameof(CdmManifestDefinition), this.Ctx as ResolveContext, $"New folder for manifest not found {newFolderPath}", "CreateResolvedManifestAsync");
                    return(null);
                }
                newManifestName = newManifestName.Substring(resolvedManifestPathSplit);
            }
            else
            {
                resolvedManifestFolder = this.Owner as CdmFolderDefinition;
            }

            Logger.Debug(nameof(CdmManifestDefinition), this.Ctx as ResolveContext, $"resolving manifest {sourceManifestPath}", "CreateResolvedManifestAsync");

            // Using the references present in the resolved entities, get an entity
            // create an imports doc with all the necessary resolved entity references and then resolve it
            var resolvedManifest = new CdmManifestDefinition(this.Ctx, newManifestName);

            // add the new document to the folder
            if (resolvedManifestFolder.Documents.Add(resolvedManifest) == null)
            {
                // when would this happen?
                return(null);
            }

            // mapping from entity path to resolved entity path for translating relationhsip paths
            Dictionary <string, string> resEntMap = new Dictionary <string, string>();

            foreach (var entity in this.Entities)
            {
                var entDef = await this.GetEntityFromReference(entity, this);

                if (entDef == null)
                {
                    Logger.Error(nameof(CdmManifestDefinition), this.Ctx as ResolveContext, $"Unable to get entity from reference", "CreateResolvedManifestAsync");
                    return(null);
                }

                // get the path from this manifest to the source entity. this will be the {f} replacement value
                string sourceEntityFullPath = this.Ctx.Corpus.Storage.CreateAbsoluteCorpusPath(entDef.InDocument.Folder.AtCorpusPath, this);
                string f = "";
                if (sourceEntityFullPath.StartsWith(sourceManifestFolderPath))
                {
                    f = sourceEntityFullPath.Substring(sourceManifestFolderPath.Length);
                }

                string newDocumentFullPath = newEntityDocumentNameFormat.Replace("{n}", entDef.EntityName);

                newDocumentFullPath = newDocumentFullPath.Replace("{f}", f);
                newDocumentFullPath = this.Ctx.Corpus.Storage.CreateAbsoluteCorpusPath(newDocumentFullPath, this);

                int    newDocumentPathSplit = newDocumentFullPath.LastIndexOf("/") + 1;
                string newDocumentPath      = newDocumentFullPath.Substring(0, newDocumentPathSplit);
                string newDocumentName      = newDocumentFullPath.Substring(newDocumentPathSplit);

                // make sure the new folder exists
                var folder = await this.Ctx.Corpus.FetchObjectAsync <CdmFolderDefinition>(newDocumentPath) as CdmFolderDefinition;

                if (folder == null)
                {
                    Logger.Error(nameof(CdmManifestDefinition), this.Ctx as ResolveContext, $"New folder not found {newDocumentPath}", "CreateResolvedManifestAsync");
                    return(null);
                }

                // Next create the resolved entity
                var resOpt = new ResolveOptions
                {
                    WrtDoc     = entDef.InDocument,
                    Directives = new AttributeResolutionDirectiveSet(new HashSet <string> {
                        "normalized", "referenceOnly"
                    })
                };

                Logger.Debug(nameof(CdmManifestDefinition), this.Ctx as ResolveContext, $"    resolving entity {sourceEntityFullPath} to document {newDocumentFullPath}", "CreateResolvedManifestAsync");

                var resolvedEntity = await entDef.CreateResolvedEntityAsync(entDef.EntityName, resOpt, folder, newDocumentName);

                if (resolvedEntity == null)
                {
                    // Fail all resolution, if any one entity resolution fails
                    return(null);
                }

                var result = entity.Copy(resOpt) as CdmEntityDeclarationDefinition;
                if (result.ObjectType == CdmObjectType.LocalEntityDeclarationDef)
                {
                    result.EntityPath = this.Ctx.Corpus.Storage.CreateRelativeCorpusPath(resolvedEntity.AtCorpusPath, resolvedManifest) ?? result.AtCorpusPath;
                }

                resolvedManifest.Entities.Add(result);

                // absolute path is needed for generating relationships
                var absoluteEntPath = this.Ctx.Corpus.Storage.CreateAbsoluteCorpusPath(result.EntityPath, resolvedManifest);
                resEntMap.Add(this.Ctx.Corpus.Storage.CreateAbsoluteCorpusPath(entDef.AtCorpusPath, entDef.InDocument), absoluteEntPath);
            }

            Logger.Debug(nameof(CdmManifestDefinition), this.Ctx as ResolveContext, $"    calculating relationships", "CreateResolvedManifestAsync");

            // calculate the entity graph for just this manifest and any submanifests
            await(this.Ctx.Corpus as CdmCorpusDefinition)._CalculateEntityGraphAsync(resolvedManifest, resEntMap);
            // stick results into the relationships list for the manifest
            // only put in relationships that are between the entities that are used in the manifest
            await resolvedManifest.PopulateManifestRelationshipsAsync(CdmRelationshipDiscoveryStyle.Exclusive);

            // needed until Matt's changes with collections where I can propigate
            resolvedManifest.IsDirty = true;
            return(resolvedManifest);
        }
Example #7
0
        override internal async Task <bool> SaveLinkedDocuments(CopyOptions options = null)
        {
            HashSet <string> links = new HashSet <string>();

            if (options == null)
            {
                options = new CopyOptions();
            }

            if (this.Imports != null)
            {
                this.Imports.ToList().ForEach(x => links.Add(x.CorpusPath));
            }
            if (this.Entities != null)
            {
                // only the local entity declarations please
                foreach (CdmEntityDeclarationDefinition def in this.Entities)
                {
                    if (def.ObjectType == CdmObjectType.LocalEntityDeclarationDef)
                    {
                        CdmLocalEntityDeclarationDefinition defImp = def as CdmLocalEntityDeclarationDefinition;
                        links.Add(defImp.EntityPath);

                        // also, partitions can have their own schemas
                        if (defImp.DataPartitions != null)
                        {
                            foreach (CdmDataPartitionDefinition part in defImp.DataPartitions)
                            {
                                if (part.SpecializedSchema != null)
                                {
                                    links.Add(part.SpecializedSchema);
                                }
                            }
                        }
                        // so can patterns
                        if (defImp.DataPartitionPatterns != null)
                        {
                            foreach (CdmDataPartitionPatternDefinition part in defImp.DataPartitionPatterns)
                            {
                                if (part.SpecializedSchema != null)
                                {
                                    links.Add(part.SpecializedSchema);
                                }
                            }
                        }
                    }
                }
            }

            // Get all Cdm documents sequentially
            List <CdmDocumentDefinition> docs = new List <CdmDocumentDefinition>();

            foreach (var link in links)
            {
                CdmDocumentDefinition document = await FetchDocumentDefinition(link);

                if (document == null)
                {
                    return(false);
                }
                docs.Add(document);
            }

            // Save all dirty Cdm documents in parallel
            IEnumerable <Task <bool> > tasks = docs.Select(doc => SaveDocumentIfDirty(doc, options));
            var results = await Task.WhenAll(tasks);

            if (this.SubManifests != null)
            {
                foreach (var subDeclaration in this.SubManifests)
                {
                    CdmManifestDefinition subManifest = await FetchDocumentDefinition(subDeclaration.Definition) as CdmManifestDefinition;

                    if (subManifest == null || !await SaveDocumentIfDirty(subManifest, options))
                    {
                        return(false);
                    }
                }
            }

            return(true);
        }