diff options
Diffstat (limited to 'main/src/addins/AspNet/Projects/AspNetFlavor.cs')
-rw-r--r-- | main/src/addins/AspNet/Projects/AspNetFlavor.cs | 723 |
1 files changed, 723 insertions, 0 deletions
diff --git a/main/src/addins/AspNet/Projects/AspNetFlavor.cs b/main/src/addins/AspNet/Projects/AspNetFlavor.cs new file mode 100644 index 0000000000..96902a8fdb --- /dev/null +++ b/main/src/addins/AspNet/Projects/AspNetFlavor.cs @@ -0,0 +1,723 @@ +// +// AspNetAppProject.cs: ASP.NET "Web Application" project type +// +// Authors: +// Michael Hutchinson <m.j.hutchinson@gmail.com> +// +// Copyright (C) 2006 Michael Hutchinson +// +// +// This source code is licenced under The MIT License: +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Xml; + +using ICSharpCode.NRefactory.TypeSystem; + +using MonoDevelop.Core; +using MonoDevelop.Core.Assemblies; +using MonoDevelop.Core.Execution; +using MonoDevelop.Core.ProgressMonitoring; +using MonoDevelop.Core.Serialization; +using MonoDevelop.Ide.Desktop; +using MonoDevelop.Ide; +using MonoDevelop.Ide.TypeSystem; +using MonoDevelop.Projects; +using MonoDevelop.AspNet.Execution; +using MonoDevelop.AspNet.WebForms; +using System.Threading.Tasks; + +namespace MonoDevelop.AspNet.Projects +{ + [DataInclude (typeof(AspNetAppProjectConfiguration))] + public class AspNetFlavor : DotNetProjectExtension + { + [ItemProperty("XspParameters", IsExternal=true)] + XspParameters xspParameters = new XspParameters (); + + WebFormsRegistrationCache registrationCache; + WebFormsCodeBehindTypeNameCache codebehindTypeNameCache; + + #region properties + + protected override void OnGetProjectTypes (HashSet<string> types) + { + types.Add ("AspNetApp"); + } + + protected override bool IsLibraryBasedProjectType { + get { return true; } + } + + public XspParameters XspParameters { + get { return xspParameters; } + } + + internal WebFormsRegistrationCache RegistrationCache { + get { + if (registrationCache == null) + registrationCache = new WebFormsRegistrationCache (Project); + return registrationCache; + } + } + + #endregion + + #region constructors + + protected override void OnInitializeNew (string languageName, ProjectCreateInformation info, XmlElement projectOptions) + { + base.OnInitializeNew (languageName, info, projectOptions); + codebehindTypeNameCache = new WebFormsCodeBehindTypeNameCache (Project); + + var binPath = info == null? (FilePath)"bin" : info.BinPath; + foreach (var cfg in Project.Configurations.Cast<DotNetProjectConfiguration> ()) + cfg.OutputDirectory = binPath; + } + + protected override SolutionItemConfiguration OnCreateConfiguration (string name) + { + var conf = new AspNetAppProjectConfiguration (name); + conf.CopyFrom (base.OnCreateConfiguration (name)); + conf.OutputDirectory = Project.BaseDirectory.IsNullOrEmpty? "bin" : (string)Project.BaseDirectory.Combine ("bin"); + return conf; + } + + public AspNetAppProjectConfiguration GetConfiguration (ConfigurationSelector configuration) + { + return (AspNetAppProjectConfiguration) Project.GetConfiguration (configuration); + } + + #endregion + + public override void Dispose () + { + codebehindTypeNameCache.Dispose (); + RegistrationCache.Dispose (); + base.Dispose (); + } + + #region build/prebuild/execute + + + protected override Task<BuildResult> OnBuild (ProgressMonitor monitor, ConfigurationSelector configuration) + { + //if no files are set to compile, then some compilers will error out + //though this is valid with ASP.NET apps, so we just avoid calling the compiler in this case + bool needsCompile = false; + foreach (ProjectFile pf in Project.Files) { + if (pf.BuildAction == BuildAction.Compile) { + needsCompile = true; + break; + } + } + + if (needsCompile) + return base.OnBuild (monitor, configuration); + return Task.FromResult (BuildResult.Success); + } + + ExecutionCommand CreateExecutionCommand (ConfigurationSelector config, AspNetAppProjectConfiguration configuration) + { + return new AspNetExecutionCommand { + ClrVersion = configuration.ClrVersion, + DebugMode = configuration.DebugMode, + XspParameters = XspParameters, + BaseDirectory = Project.BaseDirectory, + TargetRuntime = Project.TargetRuntime, + TargetFramework = Project.TargetFramework, + UserAssemblyPaths = Project.GetUserAssemblyPaths (config), + EnvironmentVariables = configuration.EnvironmentVariables, + }; + } + + protected override bool OnGetCanExecute (ExecutionContext context, ConfigurationSelector configuration) + { + var cmd = CreateExecutionCommand (configuration, GetConfiguration (configuration)); + return context.ExecutionHandler.CanExecute (cmd); + } + + protected async override Task OnExecute (ProgressMonitor monitor, ExecutionContext context, ConfigurationSelector configuration) + { + //check XSP is available + + var cfg = GetConfiguration (configuration); + var cmd = CreateExecutionCommand (configuration, cfg); + var browserExcTarget = (BrowserExecutionTarget) context.ExecutionTarget; + + IConsole console = null; + + bool isXsp = true; //FIXME: fix this when it might not be true - should delegate to the ExecutionHandler + + try { + //HACK: check XSP exists first, because error UX is cleaner w/o displaying a blank console pad. + if (isXsp) { + try { + AspNetExecutionHandler.GetXspPath ((AspNetExecutionCommand)cmd); + } catch (UserException ex) { + MessageService.ShowError ( + GettextCatalog.GetString ("Could not launch ASP.NET web server"), + ex.Message); + throw; + } + } + + if (cfg.ExternalConsole) + console = context.ExternalConsoleFactory.CreateConsole (!cfg.PauseConsoleOutput); + else + console = context.ConsoleFactory.CreateConsole (!cfg.PauseConsoleOutput); + + // The running Port value is now captured in the XspBrowserLauncherConsole object + string url = String.Format ("http://{0}", XspParameters.Address); + + + if (isXsp) { + console = new XspBrowserLauncherConsole (console, delegate (string port) { + if (browserExcTarget != null) + browserExcTarget.DesktopApp.Launch (String.Format("{0}:{1}", url, port)); + else + BrowserLauncher.LaunchDefaultBrowser (String.Format("{0}:{1}", url, port)); + }); + } + + monitor.Log.WriteLine ("Running web server..."); + + var op = context.ExecutionHandler.Execute (cmd, console); + + if (!isXsp) { + if (browserExcTarget != null) + browserExcTarget.DesktopApp.Launch (url); + else + BrowserLauncher.LaunchDefaultBrowser (url); + } + + using (monitor.CancellationToken.Register (op.Cancel)) + await op.Task; + + monitor.Log.WriteLine ("The web server exited with code: {0}", op.ExitCode); + + } catch (Exception ex) { + if (!(ex is UserException)) { + LoggingService.LogError ("Could not launch ASP.NET web server.", ex); + } + monitor.ReportError ("Could not launch web server.", ex); + } finally { + if (console != null) + console.Dispose (); + } + } + + #endregion + + #region File utility methods + + public WebSubtype DetermineWebSubtype (ProjectFile file) + { + if (Project.LanguageBinding != null && Project.LanguageBinding.IsSourceCodeFile (file.FilePath)) + return WebSubtype.Code; + return DetermineWebSubtype (file.Name); + } + + public static WebSubtype DetermineWebSubtype (string fileName) + { + string extension = Path.GetExtension (fileName); + if (extension == null) + return WebSubtype.None; + extension = extension.ToUpperInvariant ().TrimStart ('.'); + + //NOTE: No way to identify WebSubtype.Code from here + //use the instance method for that + switch (extension) { + case "ASPX": + return WebSubtype.WebForm; + case "MASTER": + return WebSubtype.MasterPage; + case "ASHX": + return WebSubtype.WebHandler; + case "ASCX": + return WebSubtype.WebControl; + case "ASMX": + return WebSubtype.WebService; + case "ASAX": + return WebSubtype.Global; + case "GIF": + case "PNG": + case "JPG": + return WebSubtype.WebImage; + case "SKIN": + return WebSubtype.WebSkin; + case "CONFIG": + return WebSubtype.Config; + case "BROWSER": + return WebSubtype.BrowserDefinition; + case "AXD": + return WebSubtype.Axd; + case "SITEMAP": + return WebSubtype.Sitemap; + case "CSS": + return WebSubtype.Css; + case "XHTML": + case "HTML": + case "HTM": + return WebSubtype.Html; + case "JS": + return WebSubtype.JavaScript; + case "LESS": + return WebSubtype.Less; + case "SASS": + case "SCSS": + return WebSubtype.Sass; + case "EOT": + case "TTF": + case "OTF": + case "WOFF": + return WebSubtype.Font; + case "SVG": + return WebSubtype.Svg; + case "STYL": + return WebSubtype.Stylus; + case "CSHTML": + return WebSubtype.Razor; + default: + return WebSubtype.None; + } + } + + #endregion + + #region special files + + #endregion + + public ProjectFile ResolveVirtualPath (string virtualPath, string relativeToFile) + { + string name = VirtualToLocalPath (virtualPath, relativeToFile); + if (name == null) + return null; + return Project.Files.GetFile (name); + } + + public string VirtualToLocalPath (string virtualPath, string relativeToFile) + { + if (string.IsNullOrEmpty (virtualPath) || virtualPath [0] == '/' || virtualPath.IndexOf (':') > -1) + return null; + + FilePath relativeToDir; + if (virtualPath.Length > 1 && virtualPath[0] == '~') { + if (virtualPath[1] == '/') + virtualPath = virtualPath.Substring (2); + else + virtualPath = virtualPath.Substring (1); + relativeToDir = Project.BaseDirectory; + } else { + relativeToDir = String.IsNullOrEmpty (relativeToFile) + ? Project.BaseDirectory + : (FilePath) Path.GetDirectoryName (relativeToFile); + } + + virtualPath = virtualPath.Replace ('/', Path.DirectorySeparatorChar); + return relativeToDir.Combine (virtualPath).FullPath; + } + + public string LocalToVirtualPath (string filename) + { + string rel = FileService.AbsoluteToRelativePath (Project.BaseDirectory, filename); + return "~/" + rel.Replace (Path.DirectorySeparatorChar, '/'); + } + + public string LocalToVirtualPath (ProjectFile file) + { + return LocalToVirtualPath (file.FilePath); + } + + #region Reference handling + + protected override void OnReferenceAddedToProject (ProjectReferenceEventArgs e) + { + //short-circuit if the project is being deserialised + if (Project.Loading) { + base.OnReferenceAddedToProject (e); + return; + } + + UpdateWebConfigRefs (); + + base.OnReferenceAddedToProject (e); + } + + protected override void OnReferenceRemovedFromProject (ProjectReferenceEventArgs e) + { + //short-circuit if the project is being deserialised + if (Project.Loading) { + base.OnReferenceAddedToProject (e); + return; + } + + UpdateWebConfigRefs (); + + base.OnReferenceRemovedFromProject (e); + } + + void UpdateWebConfigRefs () + { + var refs = new List<string> (); + foreach (var reference in Project.References) { + //local copied assemblies are copied to the bin directory so ASP.NET references them automatically + if (reference.LocalCopy && (reference.ReferenceType == ReferenceType.Project || reference.ReferenceType == ReferenceType.Assembly)) + continue; + if (string.IsNullOrEmpty (reference.Reference)) + continue; + //these assemblies are referenced automatically by ASP.NET + if (WebFormsRegistrationCache.IsDefaultReference (reference.Reference)) + continue; + //bypass non dotnet projects + if ((reference.ReferenceType == ReferenceType.Project) && + (!(reference.OwnerProject.ParentSolution.FindProjectByName (reference.Reference) is DotNetProject))) + continue; + refs.Add (reference.Reference); + } + + var webConfig = GetWebConfig (); + if (webConfig == null || !File.Exists (webConfig.FilePath)) + return; + + var textFile = TextFileProvider.Instance.GetEditableTextFile (webConfig.FilePath); + //use textfile API because it's write safe (writes out to another file then moves) + if (textFile == null) + textFile = MonoDevelop.Projects.Text.TextFile.ReadFile (webConfig.FilePath); + + //can't use System.Web.Configuration.WebConfigurationManager, as it can only access virtual paths within an app + //so need full manual handling + try { + var doc = new XmlDocument (); + + //FIXME: PreserveWhitespace doesn't handle whitespace in attribute lists + //doc.PreserveWhitespace = true; + doc.LoadXml (textFile.Text); + + //hunt our way to the assemblies element, creating elements if necessary + XmlElement configElement = doc.DocumentElement; + if (configElement == null || string.Compare (configElement.Name, "configuration", StringComparison.OrdinalIgnoreCase) != 0) { + configElement = (XmlElement) doc.AppendChild (doc.CreateNode (XmlNodeType.Document, "configuration", null)); + } + XmlElement webElement = GetNamedXmlElement (doc, configElement, "system.web"); + XmlElement compilationNode = GetNamedXmlElement (doc, webElement, "compilation"); + XmlElement assembliesNode = GetNamedXmlElement (doc, compilationNode, "assemblies"); + + List<XmlNode> existingAdds = new List<XmlNode> (); + foreach (XmlNode node in assembliesNode) + if (string.Compare (node.Name, "add", StringComparison.OrdinalIgnoreCase) == 0) + existingAdds.Add (node); + + //add refs to the doc if they're not in it + foreach (string reference in refs) { + int index = 0; + bool found = false; + while (index < existingAdds.Count) { + XmlNode node = existingAdds [index]; + XmlAttribute att = (XmlAttribute)node.Attributes.GetNamedItem ("assembly"); + if (att == null) + continue; + string refAtt = att.Value; + if (refAtt != null && refAtt == reference) { + existingAdds.RemoveAt (index); + found = true; + break; + } + index++; + } + if (!found) { + XmlElement newAdd = doc.CreateElement ("add"); + XmlAttribute newAtt = doc.CreateAttribute ("assembly"); + newAtt.Value = reference; + newAdd.Attributes.Append (newAtt); + assembliesNode.AppendChild (newAdd); + } + } + + //any nodes that weren't removed from the existingAdds list are old/redundant, so remove from doc + foreach (XmlNode node in existingAdds) + assembliesNode.RemoveChild (node); + + StringWriter sw = new StringWriter (); + XmlTextWriter tw = new XmlTextWriter (sw); + tw.Formatting = Formatting.Indented; + doc.WriteTo (tw); + tw.Flush (); + textFile.Text = sw.ToString (); + + MonoDevelop.Projects.Text.TextFile tf = textFile as MonoDevelop.Projects.Text.TextFile; + if (tf != null) + tf.Save (); + } catch (Exception e) { + LoggingService.LogWarning ("Could not modify application web.config in project " + Project.Name, e); + } + } + + + XmlElement GetNamedXmlElement (XmlDocument doc, XmlElement parent, string name) + { + XmlElement result = null; + foreach (XmlNode node in parent.ChildNodes) { + XmlElement elem = node as XmlElement; + if (elem != null && string.Compare (elem.Name, name, StringComparison.OrdinalIgnoreCase) == 0) { + result = elem; + break; + } + } + if (result == null) { + result = (XmlElement) parent.AppendChild (doc.CreateElement (name)); + } + return result; + } + + ProjectFile GetWebConfig () + { + var webConf = Project.BaseDirectory.Combine ("web.config"); + foreach (var file in Project.Files) + if (string.Compare (file.FilePath.ToString (), webConf, StringComparison.OrdinalIgnoreCase) == 0) + return file; + return null; + } + + bool IsWebConfig (FilePath file) + { + var webConf = Project.BaseDirectory.Combine ("web.config"); + return (string.Compare (file, webConf, StringComparison.OrdinalIgnoreCase) == 0); + } + + #endregion + + #region File event handlers + + protected override void OnFileAddedToProject (ProjectFileEventArgs e) + { + //short-circuit if the project is being deserialised + if (Project.Loading) { + base.OnFileAddedToProject (e); + return; + } + + bool webConfigChange = false; + List<string> filesToAdd = new List<string> (); + + foreach (ProjectFileEventInfo fargs in e) { + IEnumerable<string> files = MonoDevelop.DesignerSupport.CodeBehind.GuessDependencies + (Project, fargs.ProjectFile, groupedExtensions); + if (files != null) + filesToAdd.AddRange (files); + if (IsWebConfig (fargs.ProjectFile.FilePath)) + webConfigChange = true; + } + + if (webConfigChange) + UpdateWebConfigRefs (); + + //let the base fire the event before we add files + //don't want to fire events out of order of files being added + base.OnFileAddedToProject (e); + + //make sure that the parent and child files are in the project + foreach (string file in filesToAdd) { + //NOTE: this only adds files if they are not already in the project + Project.AddFile (file); + } + } + + protected override string OnGetDefaultBuildAction (string fileName) + { + + WebSubtype type = DetermineWebSubtype (fileName); + switch (type) { + case WebSubtype.Code: + return BuildAction.Compile; + case WebSubtype.None: + return base.OnGetDefaultBuildAction (fileName); + default: + return BuildAction.Content; + } + } + + static string[] groupedExtensions = { ".aspx", ".master", ".ashx", ".ascx", ".asmx", ".asax" }; + + #endregion + + public virtual IEnumerable<string> GetSpecialDirectories () + { + yield return "App_Browsers"; + yield return "App_Data"; + yield return "App_GlobalResources"; + yield return "App_LocalResources"; + yield return "Theme"; + + if (IsAspMvcProject) { + yield return "Views"; + yield return "Models"; + yield return "Controllers"; + } + + // For "web site" projects + // "App_WebReferences", "App_Resources","App_Themes", "App_Code", + } + + protected override IList<string> OnGetCommonBuildActions () + { + return new [] { + BuildAction.None, + BuildAction.Compile, + BuildAction.Content, + BuildAction.EmbeddedResource, + }; + } + + public string GetCodebehindTypeName (string fileName) + { + lock (codebehindTypeNameCache) + return codebehindTypeNameCache.GetCodeBehindTypeName (fileName); + } + + public IList<string> GetCodeTemplates (string type, string subtype = null) + { + var files = new List<string> (); + var names = new HashSet<string> (); + + string asmDir = Path.GetDirectoryName (GetType().Assembly.Location); + string lang = Project.LanguageName; + if (lang == "C#") { + lang = "CSharp"; + } + + if (subtype != null) { + type = Path.Combine (type, subtype); + } + + var dirs = new [] { + Path.Combine (Project.BaseDirectory, "CodeTemplates", type), + Path.Combine (Project.BaseDirectory, "CodeTemplates", lang, type), + Path.Combine (asmDir, "CodeTemplates", type), + Path.Combine (asmDir, "CodeTemplates", lang, type), + }; + + foreach (string directory in dirs) + if (Directory.Exists (directory)) + foreach (string file in Directory.GetFiles (directory, "*.tt", SearchOption.TopDirectoryOnly)) + if (names.Add (Path.GetFileName (file))) + files.Add (file); + + return files; + } + + protected override void OnPopulateSupportFileList (FileCopySet list, ConfigurationSelector configuration) + { + base.OnPopulateSupportFileList (list, configuration); + + //HACK: workaround for MD not local-copying package references + foreach (MonoDevelop.Projects.ProjectReference projectReference in Project.References) { + if (projectReference.Package != null && projectReference.Package.Name == "system.web.mvc") { + if (projectReference.ReferenceType == ReferenceType.Package) + foreach (SystemAssembly assem in projectReference.Package.Assemblies) + list.Add (assem.Location); + break; + } + } + } + + public string GetAspNetMvcVersion () + { + foreach (var pref in Project.References) { + if (pref.Reference.IndexOf ("System.Web.Mvc", StringComparison.OrdinalIgnoreCase) < 0) + continue; + switch (pref.ReferenceType) { + case ReferenceType.Assembly: + case ReferenceType.Package: + foreach (var f in pref.GetReferencedFileNames (null)) { + if (Path.GetFileNameWithoutExtension (f) != "System.Web.Mvc" || !File.Exists (f)) + continue; + return AssemblyName.GetAssemblyName (f).Version.ToString (); + } + break; + default: + continue; + } + } + + if (IsAspMvcProject) + return GetDefaultAspNetMvcVersion (); + + return null; + } + + public bool SupportsRazorViewEngine { + get { + return Project.References.Any (r => r.Reference.StartsWith ("System.Web.WebPages.Razor", StringComparison.Ordinal)); + } + } + + protected virtual string GetDefaultAspNetMvcVersion () + { + return "5.2"; + } + + public virtual bool IsAspMvcProject { + get { + return Project.References.Any (r => r.Reference.StartsWith ("System.Web.Mvc", StringComparison.Ordinal)); + } + } + + class BrowserExecutionTarget : ExecutionTarget + { + string name, id; + public BrowserExecutionTarget (string id, string displayName, DesktopApplication app){ + this.name = displayName; + this.id = id; + this.DesktopApp = app; + } + + public override string Name { + get { return name; } + } + + public override string Id { + get { return id; } + } + + public DesktopApplication DesktopApp { get; private set; } + } + + protected override IEnumerable<ExecutionTarget> OnGetExecutionTargets (ConfigurationSelector configuration) + { + var apps = new List<ExecutionTarget> (); + foreach (var browser in MonoDevelop.Ide.DesktopService.GetApplications ("test.html")) { + if (browser.IsDefault) + apps.Insert (0, new BrowserExecutionTarget (browser.Id,browser.DisplayName,browser)); + else + apps.Add (new BrowserExecutionTarget (browser.Id,browser.DisplayName,browser)); + } + return apps; + } + } +} |