// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using System.Collections.Generic; using System.IO; using System.Linq; using Microsoft.AspNet.FileProviders; using Microsoft.AspNet.Razor; using Microsoft.AspNet.Razor.Generator.Compiler; using Microsoft.AspNet.Razor.Parser; namespace Microsoft.AspNet.Mvc.Razor.Directives { /// /// A utility type for supporting inheritance of directives into a page from applicable _GlobalImport pages. /// public class ChunkInheritanceUtility { private readonly MvcRazorHost _razorHost; private readonly IReadOnlyList _defaultInheritedChunks; private readonly ICodeTreeCache _codeTreeCache; /// /// Initializes a new instance of . /// /// The used to parse _GlobalImport pages. /// that caches instances. /// /// Sequence of s inherited by default. public ChunkInheritanceUtility([NotNull] MvcRazorHost razorHost, [NotNull] ICodeTreeCache codeTreeCache, [NotNull] IReadOnlyList defaultInheritedChunks) { _razorHost = razorHost; _defaultInheritedChunks = defaultInheritedChunks; _codeTreeCache = codeTreeCache; } /// /// Gets an ordered of parsed for each /// _GlobalImport that is applicable to the page located at . The list is /// ordered so that the for the _GlobalImport closest to the /// in the file system appears first. /// /// The path of the page to locate inherited chunks for. /// A of parsed _GlobalImport /// s. public IReadOnlyList GetInheritedCodeTrees([NotNull] string pagePath) { var inheritedCodeTrees = new List(); var templateEngine = new RazorTemplateEngine(_razorHost); foreach (var globalImportPath in ViewHierarchyUtility.GetGlobalImportLocations(pagePath)) { // globalImportPath contains the app-relative path of the _GlobalImport. // Since the parsing of a _GlobalImport would cause parent _GlobalImports to be parsed // we need to ensure the paths are app-relative to allow the GetGlobalFileLocations // for the current _GlobalImport to succeed. var codeTree = _codeTreeCache.GetOrAdd(globalImportPath, fileInfo => ParseViewFile(templateEngine, fileInfo, globalImportPath)); if (codeTree != null) { inheritedCodeTrees.Add(codeTree); } } return inheritedCodeTrees; } /// /// Merges inherited by default and instances produced by parsing /// _GlobalImport files into the specified . /// /// The to merge in to. /// inherited from _GlobalImport /// files. /// The list of chunks to merge. public void MergeInheritedCodeTrees([NotNull] CodeTree codeTree, [NotNull] IReadOnlyList inheritedCodeTrees, string defaultModel) { var mergerMappings = GetMergerMappings(codeTree, defaultModel); IChunkMerger merger; // We merge chunks into the codeTree in two passes. In the first pass, we traverse the CodeTree visiting // a mapped IChunkMerger for types that are registered. foreach (var chunk in codeTree.Chunks) { if (mergerMappings.TryGetValue(chunk.GetType(), out merger)) { merger.VisitChunk(chunk); } } // In the second phase we invoke IChunkMerger.Merge for each chunk that has a mapped merger. // During this phase, the merger can either add to the CodeTree or ignore the chunk based on the merging // rules. // Read the chunks outside in - that is chunks from the _GlobalImport closest to the page get merged in first // and the furthest one last. This allows the merger to ignore a directive like @model that was previously // seen. var chunksToMerge = inheritedCodeTrees.SelectMany(tree => tree.Chunks) .Concat(_defaultInheritedChunks); foreach (var chunk in chunksToMerge) { if (mergerMappings.TryGetValue(chunk.GetType(), out merger)) { merger.Merge(codeTree, chunk); } } } private static Dictionary GetMergerMappings(CodeTree codeTree, string defaultModel) { var modelType = ChunkHelper.GetModelTypeName(codeTree, defaultModel); return new Dictionary { { typeof(UsingChunk), new UsingChunkMerger() }, { typeof(InjectChunk), new InjectChunkMerger(modelType) }, { typeof(SetBaseTypeChunk), new SetBaseTypeChunkMerger(modelType) } }; } private static CodeTree ParseViewFile(RazorTemplateEngine engine, IFileInfo fileInfo, string viewStartPath) { using (var stream = fileInfo.CreateReadStream()) { using (var streamReader = new StreamReader(stream)) { var parseResults = engine.ParseTemplate(streamReader, viewStartPath); var className = ParserHelpers.SanitizeClassName(fileInfo.Name); var language = engine.Host.CodeLanguage; var codeGenerator = language.CreateCodeGenerator(className, engine.Host.DefaultNamespace, viewStartPath, engine.Host); codeGenerator.Visit(parseResults); return codeGenerator.Context.CodeTreeBuilder.CodeTree; } } } } }