diff options
author | Lluis Sanchez Gual <lluis@xamarin.com> | 2015-04-17 18:25:28 +0300 |
---|---|---|
committer | Lluis Sanchez Gual <lluis@xamarin.com> | 2015-04-17 18:27:01 +0300 |
commit | 42b7775af9107d63082fd9f07954459d1838252f (patch) | |
tree | 53e6a6a2a96add2574ccf438fe6f020a02ad9740 | |
parent | 2c8f037e166b559d03a8540d14a0536f1c395116 (diff) |
Add support for custom data serialization in solutions
16 files changed, 940 insertions, 292 deletions
diff --git a/main/src/core/MonoDevelop.Core/MonoDevelop.Core.Serialization/ClassDataType.cs b/main/src/core/MonoDevelop.Core/MonoDevelop.Core.Serialization/ClassDataType.cs index 719e316efe..c1d0954759 100644 --- a/main/src/core/MonoDevelop.Core/MonoDevelop.Core.Serialization/ClassDataType.cs +++ b/main/src/core/MonoDevelop.Core/MonoDevelop.Core.Serialization/ClassDataType.cs @@ -313,15 +313,25 @@ namespace MonoDevelop.Core.Serialization foreach (ItemProperty prop in Properties) { if (prop.ReadOnly || !prop.CanSerialize (serCtx, obj)) continue; + + DataCollection col = itemCol; + object val = prop.GetValue (obj); - if (val == null) - continue; - if (!serCtx.IsDefaultValueSerializationForced (prop) && val.Equals (prop.DefaultValue)) + if (val == null) { + if (serCtx.IncludeDeletedValues) { + if (prop.IsNested) + col = GetNestedCollection (col, prop.NameList, 0, true); + col.Add (new DataDeletedNode (prop.SingleName)); + } continue; + } - DataCollection col = itemCol; + var isDefault = val.Equals (prop.DefaultValue); + if (isDefault && !serCtx.IsDefaultValueSerializationForced (prop)) + continue; + if (prop.IsNested) - col = GetNestedCollection (col, prop.NameList, 0); + col = GetNestedCollection (col, prop.NameList, 0, isDefault); if (prop.ExpandedCollection) { ICollectionHandler handler = prop.ExpandedCollectionHandler; @@ -336,8 +346,12 @@ namespace MonoDevelop.Core.Serialization } else { DataNode data = prop.Serialize (serCtx, obj, val); - if (data == null) + if (data == null) { + if (serCtx.IncludeDeletedValues) + col.Add (new DataDeletedNode (prop.SingleName)); continue; + } + data.IsDefaultValue = isDefault; col.Add (data); } } @@ -352,7 +366,7 @@ namespace MonoDevelop.Core.Serialization return itemCol; } - DataCollection GetNestedCollection (DataCollection col, string[] nameList, int pos) + DataCollection GetNestedCollection (DataCollection col, string[] nameList, int pos, bool isDefault) { if (pos == nameList.Length - 1) return col; @@ -361,8 +375,11 @@ namespace MonoDevelop.Core.Serialization item = new DataItem (); item.Name = nameList[pos]; col.Add (item); + item.IsDefaultValue = isDefault; } - return GetNestedCollection (item.ItemData, nameList, pos + 1); + if (item.IsDefaultValue && !isDefault) + item.IsDefaultValue = false; + return GetNestedCollection (item.ItemData, nameList, pos + 1, isDefault); } internal protected override object OnDeserialize (SerializationContext serCtx, object mapData, DataNode data) diff --git a/main/src/core/MonoDevelop.Core/MonoDevelop.Core.Serialization/DataCollection.cs b/main/src/core/MonoDevelop.Core/MonoDevelop.Core.Serialization/DataCollection.cs index f1b2f12b5d..970427f092 100644 --- a/main/src/core/MonoDevelop.Core/MonoDevelop.Core.Serialization/DataCollection.cs +++ b/main/src/core/MonoDevelop.Core/MonoDevelop.Core.Serialization/DataCollection.cs @@ -29,57 +29,28 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Collections.ObjectModel; namespace MonoDevelop.Core.Serialization { [Serializable] - public class DataCollection: IEnumerable + public sealed class DataCollection: Collection<DataNode> { - List<DataNode> list = new List<DataNode> (); - - public DataCollection () - { - } - - protected List<DataNode> List { - get { - if (list == null) - list = new List<DataNode> (); - return list; - } - } - - public int Count - { - get { return list == null ? 0 : list.Count; } - } - - public virtual DataNode this [int n] - { - get { return List[n]; } - set { List[n] = value; } - } - - public virtual DataNode this [string name] + public DataNode this [string name] { get { DataCollection col; int i = FindData (name, out col, false); - if (i != -1) return col.List [i]; + if (i != -1) return col [i]; else return null; } } int FindData (string name, out DataCollection colec, bool buildTree) { - if (list == null) { - colec = null; - return -1; - } - if (name.IndexOf ('/') == -1) { - for (int n=0; n<list.Count; n++) { - DataNode data = list [n]; + for (int n=0; n<Items.Count; n++) { + DataNode data = Items [n]; if (data.Name == name) { colec = this; return n; @@ -111,8 +82,8 @@ namespace MonoDevelop.Core.Serialization } pos = -1; - for (int n=0; n<colec.List.Count; n++) { - data = colec.List [n]; + for (int n=0; n<colec.Count; n++) { + data = colec [n]; if (data.Name == names [p]) { pos = n; break; } @@ -122,47 +93,14 @@ namespace MonoDevelop.Core.Serialization } } - public virtual IEnumerator GetEnumerator () - { - return list == null ? Type.EmptyTypes.GetEnumerator() : list.GetEnumerator (); - } - - public void AddRange (DataCollection col) - { - foreach (DataNode node in col) - Add (node); - } - - public virtual void Add (DataNode entry) - { - if (entry == null) - throw new ArgumentNullException ("entry"); - - List.Add (entry); - } - - public virtual void Insert (int index, DataNode entry) - { - if (entry == null) - throw new ArgumentNullException ("entry"); - - List.Insert (index, entry); - } - - public virtual void Add (DataNode entry, string itemPath) + public void Add (DataNode entry, string itemPath) { if (entry == null) throw new ArgumentNullException ("entry"); DataCollection col; FindData (itemPath + "/", out col, true); - col.List.Add (entry); - } - - public virtual void Remove (DataNode entry) - { - if (list != null) - list.Remove (entry); + Add (entry); } public DataNode Extract (string name) @@ -170,25 +108,13 @@ namespace MonoDevelop.Core.Serialization DataCollection col; int i = FindData (name, out col, false); if (i != -1) { - DataNode data = col.List [i]; - col.list.RemoveAt (i); + DataNode data = col [i]; + col.RemoveAt (i); return data; } return null; } - public int IndexOf (DataNode entry) - { - if (list == null) return -1; - return list.IndexOf (entry); - } - - public virtual void Clear () - { - if (list != null) - list.Clear (); - } - public void Merge (DataCollection col) { ArrayList toAdd = new ArrayList (); @@ -204,18 +130,5 @@ namespace MonoDevelop.Core.Serialization foreach (DataNode node in toAdd) Add (node); } - - // Sorts the list using the specified key order - public void Sort (Dictionary<string,int> nameToPosition) - { - list.Sort (delegate (DataNode x, DataNode y) { - int p1, p2; - if (!nameToPosition.TryGetValue (x.Name, out p1)) - p1 = int.MaxValue; - if (!nameToPosition.TryGetValue (y.Name, out p2)) - p2 = int.MaxValue; - return p1.CompareTo (p2); - }); - } } } diff --git a/main/src/core/MonoDevelop.Core/MonoDevelop.Core.Serialization/DataDeletedValue.cs b/main/src/core/MonoDevelop.Core/MonoDevelop.Core.Serialization/DataDeletedValue.cs new file mode 100644 index 0000000000..55233bc925 --- /dev/null +++ b/main/src/core/MonoDevelop.Core/MonoDevelop.Core.Serialization/DataDeletedValue.cs @@ -0,0 +1,42 @@ +// +// DataDeletedValue.cs +// +// Author: +// Lluis Sanchez Gual <lluis@xamarin.com> +// +// Copyright (c) 2015 Xamarin, Inc (http://www.xamarin.com) +// +// 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; + +namespace MonoDevelop.Core.Serialization +{ + /// <summary> + /// A data node that represents a value that has been deleted. + /// </summary> + [Serializable] + public class DataDeletedNode: DataNode + { + public DataDeletedNode (string name) + { + Name = name; + } + } +} + diff --git a/main/src/core/MonoDevelop.Core/MonoDevelop.Core.Serialization/DataItem.cs b/main/src/core/MonoDevelop.Core/MonoDevelop.Core.Serialization/DataItem.cs index 19bccb54aa..5fd5d9320d 100644 --- a/main/src/core/MonoDevelop.Core/MonoDevelop.Core.Serialization/DataItem.cs +++ b/main/src/core/MonoDevelop.Core/MonoDevelop.Core.Serialization/DataItem.cs @@ -27,6 +27,7 @@ // using System; +using System.Collections.Generic; namespace MonoDevelop.Core.Serialization { @@ -65,6 +66,30 @@ namespace MonoDevelop.Core.Serialization if (data == null) return null; return data.Extract (name); } + + internal void UpdateFromItem (DataItem item, HashSet<DataItem> removedItems) + { + foreach (var d in item.ItemData) { + var current = ItemData[d.Name]; + if (current != null) { + if (d.IsDefaultValue || d is DataDeletedNode) { + if (current is DataItem) + removedItems.Add ((DataItem)current); + ItemData.Remove (current); + } + else if (current.GetType () != d.GetType () || current is DataValue) { + var i = ItemData.IndexOf (current); + ItemData [i] = d; + if (current is DataItem) + removedItems.Add ((DataItem)current); + } else if (current is DataItem) { + ((DataItem)current).UpdateFromItem ((DataItem)d, removedItems); + } + } else if (!d.IsDefaultValue && !(d is DataDeletedNode)) { + ItemData.Add (d); + } + } + } public override string ToString () { diff --git a/main/src/core/MonoDevelop.Core/MonoDevelop.Core.Serialization/DataNode.cs b/main/src/core/MonoDevelop.Core/MonoDevelop.Core.Serialization/DataNode.cs index 1f6a847719..98b1cd9b29 100644 --- a/main/src/core/MonoDevelop.Core/MonoDevelop.Core.Serialization/DataNode.cs +++ b/main/src/core/MonoDevelop.Core/MonoDevelop.Core.Serialization/DataNode.cs @@ -44,5 +44,11 @@ namespace MonoDevelop.Core.Serialization { return ""; } + + /// <summary> + /// Gets or sets a value indicating whether this instance is default value. + /// </summary> + /// <remarks>This flag is set when an object is serialized using the IncludeDefaultValues or IncludeDeletedValues</remarks> + public bool IsDefaultValue { get; set; } } } diff --git a/main/src/core/MonoDevelop.Core/MonoDevelop.Core.Serialization/SerializationContext.cs b/main/src/core/MonoDevelop.Core/MonoDevelop.Core.Serialization/SerializationContext.cs index d3179c26b8..ffa277d0d8 100644 --- a/main/src/core/MonoDevelop.Core/MonoDevelop.Core.Serialization/SerializationContext.cs +++ b/main/src/core/MonoDevelop.Core/MonoDevelop.Core.Serialization/SerializationContext.cs @@ -81,9 +81,18 @@ namespace MonoDevelop.Core.Serialization monitor = value; } } - + + /// <summary> + /// When set to true, properties with default values are serialized + /// </summary> public bool IncludeDefaultValues { get; set; } + /// <summary> + /// When set to true, properties with default values are serialized, and properties that have + /// been removed are serialized as a DataDeletedValue. + /// </summary> + public bool IncludeDeletedValues { get; set; } + public void ResetDefaultValueSerialization () { forcedSerializationProps = null; @@ -98,7 +107,7 @@ namespace MonoDevelop.Core.Serialization public bool IsDefaultValueSerializationForced (ItemProperty prop) { - if (IncludeDefaultValues) + if (IncludeDefaultValues || IncludeDeletedValues) return true; else if (forcedSerializationProps != null) return forcedSerializationProps.Contains (prop); diff --git a/main/src/core/MonoDevelop.Core/MonoDevelop.Core.csproj b/main/src/core/MonoDevelop.Core/MonoDevelop.Core.csproj index c30bf344ae..d980d4c1ce 100644 --- a/main/src/core/MonoDevelop.Core/MonoDevelop.Core.csproj +++ b/main/src/core/MonoDevelop.Core/MonoDevelop.Core.csproj @@ -505,6 +505,8 @@ <Compile Include="MonoDevelop.Projects.Extensions\ImportRedirectTypeNode.cs" /> <Compile Include="MonoDevelop.Projects.Formats.MSBuild\FileUtil.cs" /> <Compile Include="MonoDevelop.Projects.Formats.MSBuild\DefaultMSBuildEngine.cs" /> + <Compile Include="MonoDevelop.Projects\SolutionDataSectionAttribute.cs" /> + <Compile Include="MonoDevelop.Core.Serialization\DataDeletedValue.cs" /> </ItemGroup> <ItemGroup> <None Include="Makefile.am" /> diff --git a/main/src/core/MonoDevelop.Core/MonoDevelop.Projects.Formats.MSBuild/IMSBuildPropertySet.cs b/main/src/core/MonoDevelop.Core/MonoDevelop.Projects.Formats.MSBuild/IMSBuildPropertySet.cs index c005edddbb..8bee78d40b 100644 --- a/main/src/core/MonoDevelop.Core/MonoDevelop.Projects.Formats.MSBuild/IMSBuildPropertySet.cs +++ b/main/src/core/MonoDevelop.Core/MonoDevelop.Projects.Formats.MSBuild/IMSBuildPropertySet.cs @@ -139,10 +139,10 @@ namespace MonoDevelop.Projects.Formats.MSBuild continue; object readVal = null; if (prop.ReturnType == typeof(FilePath)) { - FilePath def = prop.Attribute.DefaultValue != null ? (string)prop.Attribute.DefaultValue : (string)null; + FilePath def = (string)prop.Attribute.DefaultValue; readVal = pset.GetPathValue (prop.Name, def); } else if (prop.Attribute is ProjectPathItemProperty && prop.ReturnType == typeof(string)) { - FilePath def = prop.Attribute.DefaultValue != null ? (string)prop.Attribute.DefaultValue : (string)null; + FilePath def = (string)prop.Attribute.DefaultValue; readVal = pset.GetPathValue (prop.Name, def); readVal = readVal.ToString (); } else if (prop.ReturnType == typeof(string)) { @@ -154,6 +154,199 @@ namespace MonoDevelop.Projects.Formats.MSBuild } } + static DataContext solutionDataContext = new DataContext (); + + public static void WriteObjectProperties (this SlnPropertySet pset, object ob) + { + DataSerializer ser = new DataSerializer (solutionDataContext); + ser.SerializationContext.BaseFile = pset.ParentFile.FileName; + ser.SerializationContext.IncludeDeletedValues = true; + var data = ser.Serialize (ob, ob.GetType()) as DataItem; + if (data != null) + WriteDataItem (pset, data); + } + + public static void ReadObjectProperties (this SlnPropertySet pset, object ob) + { + DataSerializer ser = new DataSerializer (solutionDataContext); + ser.SerializationContext.BaseFile = pset.ParentFile.FileName; + var data = ReadDataItem (pset); + ser.Deserialize (ob, data); + } + + static void WriteDataItem (SlnPropertySet pset, DataItem item) + { + HashSet<DataItem> removedItems = new HashSet<DataItem> (); + Dictionary<DataNode,int> ids = new Dictionary<DataNode, int> (); + + // First of all read the existing data item, since we want to keep data that has not been modified + // The ids collection is filled with a map of items and their ids + var currentItem = ReadDataItem (pset, ids); + + // UpdateFromItem will add new data to the item, it will remove the data that has been removed, and + // will ignore unknown data that has not been set or removed + currentItem.UpdateFromItem (item, removedItems); + + // List of IDs that are not used anymore and can be reused when writing the item + var unusedIds = new Queue<int> (removedItems.Select (it => ids[it]).OrderBy (i => i)); + + // Calculate the next free id, to be used when adding new items + var usedIds = ids.Where (p => !removedItems.Contains (p.Key)).Select (p => p.Value).ToArray (); + int nextId = usedIds.Length > 0 ? usedIds.Max () + 1 : 0; + + // Clear all properties, since we are writing them again + pset.Clear (); + + foreach (DataNode val in currentItem.ItemData) + WriteDataNode (pset, "", val, ids, unusedIds, ref nextId); + } + + static void WriteDataNode (SlnPropertySet pset, string prefix, DataNode node, Dictionary<DataNode,int> ids, Queue<int> unusedIds, ref int id) + { + string name = node.Name; + string newPrefix = prefix.Length > 0 ? prefix + "." + name: name; + + if (node is DataValue) { + DataValue val = (DataValue) node; + string value = EncodeString (val.Value); + pset.SetValue (newPrefix, value); + } + else { + DataItem it = (DataItem) node; + int newId; + if (!ids.TryGetValue (node, out newId)) + newId = unusedIds.Count > 0 ? unusedIds.Dequeue () : (id++); + pset.SetValue (newPrefix, "$" + newId); + newPrefix = "$" + newId; + foreach (DataNode cn in it.ItemData) + WriteDataNode (pset, newPrefix, cn, ids, unusedIds, ref id); + } + } + + static string EncodeString (string val) + { + if (val.Length == 0) + return val; + + int i = val.IndexOfAny (new char[] {'\n','\r','\t'}); + if (i != -1 || val [0] == '@') { + StringBuilder sb = new StringBuilder (); + if (i != -1) { + int fi = val.IndexOf ('\\'); + if (fi != -1 && fi < i) i = fi; + sb.Append (val.Substring (0,i)); + } else + i = 0; + for (int n = i; n < val.Length; n++) { + char c = val [n]; + if (c == '\r') + sb.Append (@"\r"); + else if (c == '\n') + sb.Append (@"\n"); + else if (c == '\t') + sb.Append (@"\t"); + else if (c == '\\') + sb.Append (@"\\"); + else + sb.Append (c); + } + val = "@" + sb.ToString (); + } + char fc = val [0]; + char lc = val [val.Length - 1]; + if (fc == ' ' || fc == '"' || fc == '$' || lc == ' ') + val = "\"" + val + "\""; + return val; + } + + static string DecodeString (string val) + { + val = val.Trim (' ', '\t'); + if (val.Length == 0) + return val; + if (val [0] == '\"') + val = val.Substring (1, val.Length - 2); + if (val [0] == '@') { + StringBuilder sb = new StringBuilder (val.Length); + for (int n = 1; n < val.Length; n++) { + char c = val [n]; + if (c == '\\') { + c = val [++n]; + if (c == 'r') c = '\r'; + else if (c == 'n') c = '\n'; + else if (c == 't') c = '\t'; + } + sb.Append (c); + } + return sb.ToString (); + } + else + return val; + } + + static DataItem ReadDataItem (SlnPropertySet pset) + { + return ReadDataItem (pset, null); + } + + static DataItem ReadDataItem (SlnPropertySet pset, Dictionary<DataNode,int> ids) + { + DataItem it = new DataItem (); + + var lines = pset.ToArray (); + + int lineNum = 0; + int lastLine = lines.Length - 1; + while (lineNum <= lastLine) { + if (!ReadDataNode (it, lines, lastLine, "", ids, ref lineNum)) + lineNum++; + } + return it; + } + + static bool ReadDataNode (DataItem item, KeyValuePair<string,string>[] lines, int lastLine, string prefix, Dictionary<DataNode,int> ids, ref int lineNum) + { + var s = lines [lineNum]; + + string name = s.Key; + if (name.Length == 0) { + lineNum++; + return true; + } + + // Check if the line belongs to the current item + if (prefix.Length > 0) { + if (!s.Key.StartsWith (prefix + ".", StringComparison.Ordinal)) + return false; + name = s.Key.Substring (prefix.Length + 1); + } else { + if (s.Key.StartsWith ("$", StringComparison.Ordinal)) + return false; + } + + string value = s.Value; + if (value.StartsWith ("$", StringComparison.Ordinal)) { + // New item + DataItem child = new DataItem (); + child.Name = name; + int id; + if (ids != null && int.TryParse (value.Substring (1), out id)) + ids [child] = id; + lineNum++; + while (lineNum <= lastLine) { + if (!ReadDataNode (child, lines, lastLine, value, ids, ref lineNum)) + break; + } + item.ItemData.Add (child); + } else { + value = DecodeString (value); + DataValue val = new DataValue (name, value); + item.ItemData.Add (val); + lineNum++; + } + return true; + } + internal static void WriteExternalProjectProperties (this MSBuildProject project, object ob, Type typeToScan, bool includeBaseMembers = false) { var props = GetMembers (typeToScan, includeBaseMembers); diff --git a/main/src/core/MonoDevelop.Core/MonoDevelop.Projects.Formats.MSBuild/SlnFile.cs b/main/src/core/MonoDevelop.Core/MonoDevelop.Projects.Formats.MSBuild/SlnFile.cs index f1af02ae84..a76cb4f75c 100644 --- a/main/src/core/MonoDevelop.Core/MonoDevelop.Projects.Formats.MSBuild/SlnFile.cs +++ b/main/src/core/MonoDevelop.Core/MonoDevelop.Projects.Formats.MSBuild/SlnFile.cs @@ -8,6 +8,7 @@ using System.Collections; using MonoDevelop.Core; using MonoDevelop.Projects.Text; using System.Text.RegularExpressions; +using System.Globalization; namespace MonoDevelop.Projects.Formats.MSBuild { @@ -34,6 +35,8 @@ namespace MonoDevelop.Projects.Formats.MSBuild public SlnFile () { + projects.ParentFile = this; + sections.ParentFile = this; } /// <summary> @@ -43,7 +46,7 @@ namespace MonoDevelop.Projects.Formats.MSBuild /// <param name="file">File.</param> public static string GetFileVersion (string file) { - string strVersion = null; + string strVersion; using (var reader = new StreamReader (file)) { var strInput = reader.ReadLine(); if (strInput == null) @@ -70,21 +73,32 @@ namespace MonoDevelop.Projects.Formats.MSBuild /// The directory to be used as base for converting absolute paths to relative /// </summary> public FilePath BaseDirectory { - get; - set; + get { return FileName.ParentDirectory; } } + /// <summary> + /// Gets the solution configurations section. + /// </summary> + /// <value>The solution configurations section.</value> public SlnPropertySet SolutionConfigurationsSection { - get { return sections.GetOrCreateSection ("SolutionConfigurationPlatforms", "preSolution").Properties; } + get { return sections.GetOrCreateSection ("SolutionConfigurationPlatforms", SlnSectionType.PreProcess).Properties; } } + /// <summary> + /// Gets the project configurations section. + /// </summary> + /// <value>The project configurations section.</value> public SlnPropertySetCollection ProjectConfigurationsSection { - get { return sections.GetOrCreateSection ("ProjectConfigurationPlatforms", "postSolution").NestedPropertySets; } + get { return sections.GetOrCreateSection ("ProjectConfigurationPlatforms", SlnSectionType.PostProcess).NestedPropertySets; } } + /// <summary> + /// Gets the custom MonoDevelop properties section + /// </summary> + /// <value>The custom mono develop properties.</value> public SlnPropertySet CustomMonoDevelopProperties { get { - var s = sections.GetOrCreateSection ("MonoDevelopProperties", "preSolution"); + var s = sections.GetOrCreateSection ("MonoDevelopProperties", SlnSectionType.PreProcess); s.SkipIfEmpty = true; return s.Properties; } @@ -98,8 +112,11 @@ namespace MonoDevelop.Projects.Formats.MSBuild get { return projects; } } + public FilePath FileName { get; set; } + public void Read (string file) { + FileName = file; format = FileUtil.GetTextFormatInfo (file); using (var sr = new StreamReader (file)) @@ -116,19 +133,19 @@ namespace MonoDevelop.Projects.Formats.MSBuild while ((line = reader.ReadLine ()) != null) { curLineNum++; line = line.Trim (); - if (line.StartsWith ("Microsoft Visual Studio Solution File")) { + if (line.StartsWith ("Microsoft Visual Studio Solution File", StringComparison.Ordinal)) { int i = line.LastIndexOf (' '); if (i == -1) throw new InvalidSolutionFormatException (curLineNum); FormatVersion = line.Substring (i + 1); prefixBlankLines = curLineNum - 1; } - if (line.StartsWith ("# ")) { + if (line.StartsWith ("# ", StringComparison.Ordinal)) { if (!productRead) { productRead = true; ProductDescription = line.Substring (2); } - } else if (line.StartsWith ("Project")) { + } else if (line.StartsWith ("Project", StringComparison.Ordinal)) { SlnProject p = new SlnProject (); p.Read (reader, line, ref curLineNum); projects.Add (p); @@ -141,7 +158,7 @@ namespace MonoDevelop.Projects.Formats.MSBuild line = line.Trim (); if (line == "EndGlobal") { break; - } else if (line.StartsWith ("GlobalSection")) { + } else if (line.StartsWith ("GlobalSection", StringComparison.Ordinal)) { var sec = new SlnSection (); sec.Read (reader, line, ref curLineNum); sections.Add (sec); @@ -160,6 +177,7 @@ namespace MonoDevelop.Projects.Formats.MSBuild public void Write (string file) { + FileName = file; var sw = new StringWriter (); Write (sw); TextFile.WriteFile (file, sw.ToString(), format.ByteOrderMark, true); @@ -189,6 +207,18 @@ namespace MonoDevelop.Projects.Formats.MSBuild { SlnSectionCollection sections = new SlnSectionCollection (); + SlnFile parentFile; + + public SlnFile ParentFile { + get { + return parentFile; + } + internal set { + parentFile = value; + sections.ParentFile = parentFile; + } + } + public string Id { get; set; } public string TypeGuid { get; set; } public string Name { get; set; } @@ -241,7 +271,7 @@ namespace MonoDevelop.Projects.Formats.MSBuild if (line == "EndProject") { return; } - if (line.StartsWith ("ProjectSection")) { + if (line.StartsWith ("ProjectSection", StringComparison.Ordinal)) { if (sections == null) sections = new SlnSectionCollection (); var sec = new SlnSection (); @@ -288,8 +318,11 @@ namespace MonoDevelop.Projects.Formats.MSBuild public string Id { get; set; } public int Line { get; private set; } + internal bool Processed { get; set; } + public SlnFile ParentFile { get; internal set; } + public bool IsEmpty { get { return (properties == null || properties.Count == 0) && (nestedPropertySets == null || nestedPropertySets.All (t => t.IsEmpty)); @@ -313,6 +346,7 @@ namespace MonoDevelop.Projects.Formats.MSBuild get { if (properties == null) { properties = new SlnPropertySet (); + properties.ParentSection = this; if (sectionLines != null) { foreach (var line in sectionLines) properties.ReadLine (line, Line); @@ -326,7 +360,7 @@ namespace MonoDevelop.Projects.Formats.MSBuild public SlnPropertySetCollection NestedPropertySets { get { if (nestedPropertySets == null) { - nestedPropertySets = new SlnPropertySetCollection (); + nestedPropertySets = new SlnPropertySetCollection (this); if (sectionLines != null) LoadPropertySets (); } @@ -334,7 +368,24 @@ namespace MonoDevelop.Projects.Formats.MSBuild } } - public string SectionType { get; set; } + public SlnSectionType SectionType { get; set; } + + SlnSectionType ToSectionType (int curLineNum, string s) + { + if (s == "preSolution" || s == "preProject") + return SlnSectionType.PreProcess; + if (s == "postSolution" || s == "postProject") + return SlnSectionType.PostProcess; + throw new InvalidSolutionFormatException (curLineNum, "Invalid section type: " + s); + } + + string FromSectionType (bool isProjectSection, SlnSectionType type) + { + if (type == SlnSectionType.PreProcess) + return isProjectSection ? "preProject" : "preSolution"; + else + return isProjectSection ? "postProject" : "postSolution"; + } internal void Read (TextReader reader, string line, ref int curLineNum) { @@ -349,7 +400,7 @@ namespace MonoDevelop.Projects.Formats.MSBuild Id = line.Substring (k + 1, k2 - k - 1); k = line.IndexOf ('=', k2); - SectionType = line.Substring (k + 1).Trim (); + SectionType = ToSectionType (curLineNum, line.Substring (k + 1).Trim ()); var endTag = "End" + tag; @@ -396,7 +447,7 @@ namespace MonoDevelop.Projects.Formats.MSBuild writer.Write ('('); writer.Write (Id); writer.Write (") = "); - writer.WriteLine (SectionType); + writer.WriteLine (FromSectionType (sectionTag == "ProjectSection", SectionType)); if (sectionLines != null) { foreach (var l in sectionLines) writer.WriteLine ("\t\t" + l); @@ -410,18 +461,36 @@ namespace MonoDevelop.Projects.Formats.MSBuild } } + /// <summary> + /// A collection of properties + /// </summary> public class SlnPropertySet: IDictionary<string,string> { OrderedDictionary values = new OrderedDictionary (); bool isMetadata; internal bool Processed { get; set; } + + public SlnFile ParentFile { + get { return ParentSection != null ? ParentSection.ParentFile : null; } + } + + public SlnSection ParentSection { get; set; } + + /// <summary> + /// Text file line of this section in the original file + /// </summary> + /// <value>The line.</value> public int Line { get; private set; } - public SlnPropertySet () + internal SlnPropertySet () { } + /// <summary> + /// Creates a new property set with the specified ID + /// </summary> + /// <param name="id">Identifier.</param> public SlnPropertySet (string id) { Id = id; @@ -432,6 +501,10 @@ namespace MonoDevelop.Projects.Formats.MSBuild this.isMetadata = isMetadata; } + /// <summary> + /// Gets a value indicating whether this property set is empty. + /// </summary> + /// <value><c>true</c> if this instance is empty; otherwise, <c>false</c>.</value> public bool IsEmpty { get { return values.Count == 0; @@ -464,28 +537,163 @@ namespace MonoDevelop.Projects.Formats.MSBuild } } + /// <summary> + /// Gets the identifier of the property set + /// </summary> + /// <value>The identifier.</value> public string Id { get; private set; } - public string GetValue (string key) + public string GetValue (string name, string defaultValue = null) { - return (string) values [key]; + string res; + if (TryGetValue (name, out res)) + return res; + else + return defaultValue; } - public void SetValue (string key, string value) + public FilePath GetPathValue (string name, FilePath defaultValue = default(FilePath), bool relativeToSolution = true, FilePath relativeToPath = default(FilePath)) { - values [key] = value; + string val; + if (TryGetValue (name, out val)) { + string baseDir = null; + if (relativeToPath != null) { + baseDir = relativeToPath; + } else if (relativeToSolution && ParentFile != null && ParentFile.FileName != null) { + baseDir = ParentFile.FileName.ParentDirectory; + } + return MSBuildProjectService.FromMSBuildPath (baseDir, val); + } + else + return defaultValue; + } + + public bool TryGetPathValue (string name, out FilePath value, FilePath defaultValue = default(FilePath), bool relativeToSolution = true, FilePath relativeToPath = default(FilePath)) + { + string val; + if (TryGetValue (name, out val)) { + string baseDir = null; + + if (relativeToPath != null) { + baseDir = relativeToPath; + } else if (relativeToSolution && ParentFile != null && ParentFile.FileName != null) { + baseDir = ParentFile.FileName.ParentDirectory; + } + string path; + var res = MSBuildProjectService.FromMSBuildPath (baseDir, val, out path); + value = path; + return res; + } + else { + value = defaultValue; + return value != default(FilePath); + } + } + + public T GetValue<T> (string name) + { + return (T) GetValue (name, typeof(T), default(T)); + } + + public T GetValue<T> (string name, T defaultValue) + { + return (T) GetValue (name, typeof(T), defaultValue); + } + + public object GetValue (string name, Type t, object defaultValue) + { + string val; + if (TryGetValue (name, out val)) { + if (t == typeof(bool)) + return (object) val.Equals ("true", StringComparison.InvariantCultureIgnoreCase); + if (t.IsEnum) + return Enum.Parse (t, val, true); + if (t.IsGenericType && t.GetGenericTypeDefinition () == typeof(Nullable<>)) { + var at = t.GetGenericArguments () [0]; + if (string.IsNullOrEmpty (val)) + return null; + return Convert.ChangeType (val, at, CultureInfo.InvariantCulture); + + } + return Convert.ChangeType (val, t, CultureInfo.InvariantCulture); + } + else + return defaultValue; + } + + public void SetValue (string name, string value, string defaultValue = null, bool preserveExistingCase = false) + { + if (value == null && defaultValue == "") + value = ""; + if (value == defaultValue) { + // if the value is default, only remove the property if it was not already the default + // to avoid unnecessary project file churn + string res; + if (TryGetValue (name, out res) && !string.Equals (defaultValue ?? "", res, preserveExistingCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal)) + Remove (name); + return; + } + string currentValue; + if (preserveExistingCase && TryGetValue (name, out currentValue) && string.Equals (value, currentValue, StringComparison.OrdinalIgnoreCase)) + return; + values [name] = value; + } + + public void SetValue (string name, FilePath value, FilePath defaultValue = default(FilePath), bool relativeToSolution = true, FilePath relativeToPath = default(FilePath)) + { + var isDefault = value.CanonicalPath == defaultValue.CanonicalPath; + if (isDefault) { + // if the value is default, only remove the property if it was not already the default + // to avoid unnecessary project file churn + if (ContainsKey (name) && (defaultValue == null || defaultValue != GetPathValue (name, relativeToSolution:relativeToSolution, relativeToPath:relativeToPath))) + Remove (name); + return; + } + string baseDir = null; + if (relativeToPath != null) { + baseDir = relativeToPath; + } else if (relativeToSolution && ParentFile != null && ParentFile.FileName != null) { + baseDir = ParentFile.FileName.ParentDirectory; + } + values [name] = MSBuildProjectService.ToMSBuildPath (baseDir, value, false); + } + + public void SetValue (string name, object value, object defaultValue = null) + { + var isDefault = object.Equals (value, defaultValue); + if (isDefault) { + // if the value is default, only remove the property if it was not already the default + // to avoid unnecessary project file churn + if (ContainsKey (name) && (defaultValue == null || !object.Equals (defaultValue, GetValue (name, defaultValue.GetType (), null)))) + Remove (name); + return; + } + + if (value is bool) + values [name] = (bool)value ? "TRUE" : "FALSE"; + else + values [name] = Convert.ToString (value, CultureInfo.InvariantCulture); } - public void Add (string key, string value) + void IDictionary<string,string>.Add (string key, string value) { SetValue (key, value); } + /// <summary> + /// Determines whether the current instance contains an entry with the specified key + /// </summary> + /// <returns><c>true</c>, if key was containsed, <c>false</c> otherwise.</returns> + /// <param name="key">Key.</param> public bool ContainsKey (string key) { return values.Contains (key); } + /// <summary> + /// Removes a property + /// </summary> + /// <param name="key">Property name</param> public bool Remove (string key) { var wasThere = values.Contains (key); @@ -493,12 +701,22 @@ namespace MonoDevelop.Projects.Formats.MSBuild return wasThere; } + /// <summary> + /// Tries to get the value of a property + /// </summary> + /// <returns><c>true</c>, if the property exists, <c>false</c> otherwise.</returns> + /// <param name="key">Property name</param> + /// <param name="value">Value.</param> public bool TryGetValue (string key, out string value) { value = (string) values [key]; return value != null; } + /// <summary> + /// Gets or sets the value of a property + /// </summary> + /// <param name="index">Index.</param> public string this [string index] { get { return (string) values [index]; @@ -518,7 +736,7 @@ namespace MonoDevelop.Projects.Formats.MSBuild get { return values.Keys.Cast<string> ().ToList (); } } - public void Add (KeyValuePair<string, string> item) + void ICollection<KeyValuePair<string, string>>.Add (KeyValuePair<string, string> item) { SetValue (item.Key, item.Value); } @@ -534,7 +752,7 @@ namespace MonoDevelop.Projects.Formats.MSBuild values.Remove (k); } - public bool Contains (KeyValuePair<string, string> item) + bool ICollection<KeyValuePair<string, string>>.Contains (KeyValuePair<string, string> item) { var val = GetValue (item.Key); return val == item.Value; @@ -546,9 +764,9 @@ namespace MonoDevelop.Projects.Formats.MSBuild array [arrayIndex++] = new KeyValuePair<string, string> ((string)de.Key, (string)de.Value); } - public bool Remove (KeyValuePair<string, string> item) + bool ICollection<KeyValuePair<string, string>>.Remove (KeyValuePair<string, string> item) { - if (Contains (item)) { + if (((ICollection<KeyValuePair<string, string>>)this).Contains (item)) { Remove (item.Key); return true; } else @@ -561,7 +779,14 @@ namespace MonoDevelop.Projects.Formats.MSBuild } } - public bool IsReadOnly { + internal void SetLines (IEnumerable<KeyValuePair<string,string>> lines) + { + values.Clear (); + foreach (var line in lines) + values [line.Key] = line.Value; + } + + bool ICollection<KeyValuePair<string, string>>.IsReadOnly { get { return false; } @@ -582,6 +807,19 @@ namespace MonoDevelop.Projects.Formats.MSBuild public class SlnProjectCollection: Collection<SlnProject> { + SlnFile parentFile; + + internal SlnFile ParentFile { + get { + return parentFile; + } + set { + parentFile = value; + foreach (var it in this) + it.ParentFile = parentFile; + } + } + public SlnProject GetProject (string id) { return this.FirstOrDefault (s => s.Id == id); @@ -596,16 +834,60 @@ namespace MonoDevelop.Projects.Formats.MSBuild } return p; } + + protected override void InsertItem (int index, SlnProject item) + { + base.InsertItem (index, item); + item.ParentFile = ParentFile; + } + + protected override void SetItem (int index, SlnProject item) + { + base.SetItem (index, item); + item.ParentFile = ParentFile; + } + + protected override void RemoveItem (int index) + { + var it = this [index]; + it.ParentFile = null; + base.RemoveItem (index); + } + + protected override void ClearItems () + { + foreach (var it in this) + it.ParentFile = null; + base.ClearItems (); + } } public class SlnSectionCollection: Collection<SlnSection> { + SlnFile parentFile; + + internal SlnFile ParentFile { + get { + return parentFile; + } + set { + parentFile = value; + foreach (var it in this) + it.ParentFile = parentFile; + } + } + public SlnSection GetSection (string id) { return this.FirstOrDefault (s => s.Id == id); } - public SlnSection GetOrCreateSection (string id, string sectionType) + public SlnSection GetSection (string id, SlnSectionType sectionType) + { + return this.FirstOrDefault (s => s.Id == id && s.SectionType == sectionType); + } + + public SlnSection GetOrCreateSection (string id, SlnSectionType sectionType) { if (id == null) throw new ArgumentNullException ("id"); @@ -626,10 +908,43 @@ namespace MonoDevelop.Projects.Formats.MSBuild if (s != null) Remove (s); } + + protected override void InsertItem (int index, SlnSection item) + { + base.InsertItem (index, item); + item.ParentFile = ParentFile; + } + + protected override void SetItem (int index, SlnSection item) + { + base.SetItem (index, item); + item.ParentFile = ParentFile; + } + + protected override void RemoveItem (int index) + { + var it = this [index]; + it.ParentFile = null; + base.RemoveItem (index); + } + + protected override void ClearItems () + { + foreach (var it in this) + it.ParentFile = null; + base.ClearItems (); + } } public class SlnPropertySetCollection: Collection<SlnPropertySet> { + SlnSection parentSection; + + internal SlnPropertySetCollection (SlnSection parentSection) + { + this.parentSection = parentSection; + } + public SlnPropertySet GetPropertySet (string id, bool ignoreCase = false) { var sc = ignoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal; @@ -645,6 +960,32 @@ namespace MonoDevelop.Projects.Formats.MSBuild } return ps; } + + protected override void InsertItem (int index, SlnPropertySet item) + { + base.InsertItem (index, item); + item.ParentSection = parentSection; + } + + protected override void SetItem (int index, SlnPropertySet item) + { + base.SetItem (index, item); + item.ParentSection = parentSection; + } + + protected override void RemoveItem (int index) + { + var it = this [index]; + it.ParentSection = null; + base.RemoveItem (index); + } + + protected override void ClearItems () + { + foreach (var it in this) + it.ParentSection = null; + base.ClearItems (); + } } class InvalidSolutionFormatException: Exception @@ -655,7 +996,14 @@ namespace MonoDevelop.Projects.Formats.MSBuild public InvalidSolutionFormatException (int line, string msg): base ("Invalid format in line " + line + ": " + msg) { + } } + + public enum SlnSectionType + { + PreProcess, + PostProcess + } } diff --git a/main/src/core/MonoDevelop.Core/MonoDevelop.Projects.Formats.MSBuild/SlnFileFormat.cs b/main/src/core/MonoDevelop.Core/MonoDevelop.Projects.Formats.MSBuild/SlnFileFormat.cs index 6249e8c2c6..8188607c13 100644 --- a/main/src/core/MonoDevelop.Core/MonoDevelop.Projects.Formats.MSBuild/SlnFileFormat.cs +++ b/main/src/core/MonoDevelop.Core/MonoDevelop.Projects.Formats.MSBuild/SlnFileFormat.cs @@ -106,8 +106,6 @@ namespace MonoDevelop.Projects.Formats.MSBuild void WriteFileInternal (string file, string sourceFile, Solution solution, bool saveProjects, ProgressMonitor monitor) { - string baseDir = Path.GetDirectoryName (sourceFile); - if (saveProjects) { var items = solution.GetAllSolutionItems ().ToArray (); monitor.BeginTask (items.Length + 1); @@ -126,7 +124,7 @@ namespace MonoDevelop.Projects.Formats.MSBuild } SlnFile sln = new SlnFile (); - sln.BaseDirectory = baseDir; + sln.FileName = file; if (File.Exists (sourceFile)) { try { sln.Read (sourceFile); @@ -196,7 +194,7 @@ namespace MonoDevelop.Projects.Formats.MSBuild ICollection<SolutionFolder> folders = solution.RootFolder.GetAllItems<SolutionFolder> ().ToList (); if (folders.Count > 1) { // If folders ==1, that's the root folder - var sec = sln.Sections.GetOrCreateSection ("NestedProjects", "preSolution"); + var sec = sln.Sections.GetOrCreateSection ("NestedProjects", SlnSectionType.PreProcess); foreach (SolutionFolder folder in folders) { if (folder.IsRoot) continue; @@ -211,7 +209,7 @@ namespace MonoDevelop.Projects.Formats.MSBuild // Write custom properties for configurations foreach (SolutionConfiguration conf in solution.Configurations) { string secId = "MonoDevelopProperties." + conf.Id; - var sec = sln.Sections.GetOrCreateSection (secId, "preSolution"); + var sec = sln.Sections.GetOrCreateSection (secId, SlnSectionType.PreProcess); solution.WriteConfigurationData (monitor, sec.Properties, conf); if (sec.IsEmpty) sln.Sections.Remove (sec); @@ -233,12 +231,12 @@ namespace MonoDevelop.Projects.Formats.MSBuild proj.Name = item.Name; proj.FilePath = FileService.NormalizeRelativePath (FileService.AbsoluteToRelativePath (sln.BaseDirectory, item.FileName)).Replace ('/', '\\'); - var sec = proj.Sections.GetOrCreateSection ("MonoDevelopProperties", "preProject"); + var sec = proj.Sections.GetOrCreateSection ("MonoDevelopProperties", SlnSectionType.PreProcess); sec.SkipIfEmpty = true; folder.ParentSolution.WriteSolutionFolderItemData (monitor, sec.Properties, ce); if (item.ItemDependencies.Count > 0) { - sec = proj.Sections.GetOrCreateSection ("ProjectDependencies", "postProject"); + sec = proj.Sections.GetOrCreateSection ("ProjectDependencies", SlnSectionType.PostProcess); sec.Properties.ClearExcept (unknownProjects); foreach (var dep in item.ItemDependencies) sec.Properties.SetValue (dep.ItemId, dep.ItemId); @@ -254,7 +252,7 @@ namespace MonoDevelop.Projects.Formats.MSBuild WriteFolderFiles (proj, (SolutionFolder) ce); //Write custom properties - var sec = proj.Sections.GetOrCreateSection ("MonoDevelopProperties", "preProject"); + var sec = proj.Sections.GetOrCreateSection ("MonoDevelopProperties", SlnSectionType.PreProcess); sec.SkipIfEmpty = true; folder.ParentSolution.WriteSolutionFolderItemData (monitor, sec.Properties, ce); } @@ -267,7 +265,7 @@ namespace MonoDevelop.Projects.Formats.MSBuild void WriteFolderFiles (SlnProject proj, SolutionFolder folder) { if (folder.Files.Count > 0) { - var sec = proj.Sections.GetOrCreateSection ("SolutionItems", "preProject"); + var sec = proj.Sections.GetOrCreateSection ("SolutionItems", SlnSectionType.PreProcess); sec.Properties.Clear (); foreach (FilePath f in folder.Files) { string relFile = MSBuildProjectService.ToMSBuildPathRelative (folder.ParentSolution.ItemDirectory, f); @@ -349,151 +347,7 @@ namespace MonoDevelop.Projects.Formats.MSBuild sln.ReadSolutionFolderItemData (monitor, sec.Properties, item); } - - void WriteDataItem (SlnPropertySet pset, DataItem item) - { - pset.Clear (); - int id = 0; - foreach (DataNode val in item.ItemData) - WriteDataNode (pset, "", val, ref id); - } - - void WriteDataNode (SlnPropertySet pset, string prefix, DataNode node, ref int id) - { - string name = node.Name; - string newPrefix = prefix.Length > 0 ? prefix + "." + name: name; - - if (node is DataValue) { - DataValue val = (DataValue) node; - string value = EncodeString (val.Value); - pset.SetValue (newPrefix, value); - } - else { - DataItem it = (DataItem) node; - pset.SetValue (newPrefix, "$" + id); - newPrefix = "$" + id; - id ++; - foreach (DataNode cn in it.ItemData) - WriteDataNode (pset, newPrefix, cn, ref id); - } - } - - string EncodeString (string val) - { - if (val.Length == 0) - return val; - - int i = val.IndexOfAny (new char[] {'\n','\r','\t'}); - if (i != -1 || val [0] == '@') { - StringBuilder sb = new StringBuilder (); - if (i != -1) { - int fi = val.IndexOf ('\\'); - if (fi != -1 && fi < i) i = fi; - sb.Append (val.Substring (0,i)); - } else - i = 0; - for (int n = i; n < val.Length; n++) { - char c = val [n]; - if (c == '\r') - sb.Append (@"\r"); - else if (c == '\n') - sb.Append (@"\n"); - else if (c == '\t') - sb.Append (@"\t"); - else if (c == '\\') - sb.Append (@"\\"); - else - sb.Append (c); - } - val = "@" + sb.ToString (); - } - char fc = val [0]; - char lc = val [val.Length - 1]; - if (fc == ' ' || fc == '"' || fc == '$' || lc == ' ') - val = "\"" + val + "\""; - return val; - } - - string DecodeString (string val) - { - val = val.Trim (' ', '\t'); - if (val.Length == 0) - return val; - if (val [0] == '\"') - val = val.Substring (1, val.Length - 2); - if (val [0] == '@') { - StringBuilder sb = new StringBuilder (val.Length); - for (int n = 1; n < val.Length; n++) { - char c = val [n]; - if (c == '\\') { - c = val [++n]; - if (c == 'r') c = '\r'; - else if (c == 'n') c = '\n'; - else if (c == 't') c = '\t'; - } - sb.Append (c); - } - return sb.ToString (); - } - else - return val; - } - - DataItem ReadDataItem (SlnSection sec) - { - DataItem it = new DataItem (); - - var lines = sec.Properties.ToArray (); - int lineNum = 0; - int lastLine = lines.Length - 1; - while (lineNum <= lastLine) { - if (!ReadDataNode (it, lines, lastLine, "", ref lineNum)) - lineNum++; - } - return it; - } - - bool ReadDataNode (DataItem item, KeyValuePair<string,string>[] lines, int lastLine, string prefix, ref int lineNum) - { - var s = lines [lineNum]; - - // Check if the line belongs to the current item - if (prefix.Length > 0) { - if (!s.Key.StartsWith (prefix + ".")) - return false; - } else { - if (s.Key.StartsWith ("$")) - return false; - } - - string name = s.Key; - if (name.Length == 0) { - lineNum++; - return true; - } - - string value = s.Value; - if (value.StartsWith ("$")) { - // New item - DataItem child = new DataItem (); - child.Name = name; - lineNum++; - while (lineNum <= lastLine) { - if (!ReadDataNode (child, lines, lastLine, value, ref lineNum)) - break; - } - item.ItemData.Add (child); - } - else { - value = DecodeString (value); - DataValue val = new DataValue (name, value); - item.ItemData.Add (val); - lineNum++; - } - return true; - } - string ToSlnConfigurationId (ItemConfiguration configuration) { if (configuration.Platform.Length == 0) @@ -897,7 +751,7 @@ namespace MonoDevelop.Projects.Formats.MSBuild void LoadNestedProjects (SlnSection sec, IDictionary<string, SolutionFolderItem> entries, ProgressMonitor monitor) { - if (sec == null || String.Compare (sec.SectionType, "preSolution", StringComparison.OrdinalIgnoreCase) != 0) + if (sec == null || sec.SectionType != SlnSectionType.PreProcess) return; foreach (var kvp in sec.Properties) { diff --git a/main/src/core/MonoDevelop.Core/MonoDevelop.Projects/Solution.cs b/main/src/core/MonoDevelop.Core/MonoDevelop.Projects/Solution.cs index 6f5d3f6484..32cbc108c0 100644 --- a/main/src/core/MonoDevelop.Core/MonoDevelop.Projects/Solution.cs +++ b/main/src/core/MonoDevelop.Core/MonoDevelop.Projects/Solution.cs @@ -923,6 +923,7 @@ namespace MonoDevelop.Projects internal void ReadSolution (ProgressMonitor monitor, SlnFile file) { SolutionExtension.OnReadSolution (monitor, file); + file.CustomMonoDevelopProperties.ReadObjectProperties (this); } /*protected virtual*/ void OnReadSolution (ProgressMonitor monitor, SlnFile file) @@ -960,6 +961,7 @@ namespace MonoDevelop.Projects /*protected virtual*/ void OnWriteSolution (ProgressMonitor monitor, SlnFile file) { FileFormat.SlnFileFormat.WriteFileInternal (file, this, monitor); + file.CustomMonoDevelopProperties.WriteObjectProperties (this); } internal void WriteConfigurationData (ProgressMonitor monitor, SlnPropertySet properties, SolutionConfiguration configuration) diff --git a/main/src/core/MonoDevelop.Core/MonoDevelop.Projects/SolutionDataSectionAttribute.cs b/main/src/core/MonoDevelop.Core/MonoDevelop.Projects/SolutionDataSectionAttribute.cs new file mode 100644 index 0000000000..a772ae4f2c --- /dev/null +++ b/main/src/core/MonoDevelop.Core/MonoDevelop.Projects/SolutionDataSectionAttribute.cs @@ -0,0 +1,44 @@ +// +// SolutionDataSectionAttribute.cs +// +// Author: +// Lluis Sanchez Gual <lluis@xamarin.com> +// +// Copyright (c) 2015 Xamarin, Inc (http://www.xamarin.com) +// +// 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 MonoDevelop.Projects.Formats.MSBuild; + +namespace MonoDevelop.Projects +{ + public class SolutionDataSectionAttribute: Attribute + { + public SolutionDataSectionAttribute (string sectionName, SlnSectionType processOrder = SlnSectionType.PostProcess) + { + SectionName = sectionName; + ProcessOrder = processOrder; + } + + public string SectionName { get; set; } + + public SlnSectionType ProcessOrder { get; set; } + } +} + diff --git a/main/src/core/MonoDevelop.Core/MonoDevelop.Projects/SolutionExtension.cs b/main/src/core/MonoDevelop.Core/MonoDevelop.Projects/SolutionExtension.cs index 5126eb308d..545f2f8651 100644 --- a/main/src/core/MonoDevelop.Core/MonoDevelop.Projects/SolutionExtension.cs +++ b/main/src/core/MonoDevelop.Core/MonoDevelop.Projects/SolutionExtension.cs @@ -88,7 +88,20 @@ namespace MonoDevelop.Projects internal protected virtual void OnReadSolution (ProgressMonitor monitor, SlnFile file) { + var secAttribute = (SolutionDataSectionAttribute) Attribute.GetCustomAttribute (GetType(), typeof(SolutionDataSectionAttribute)); + if (secAttribute != null && secAttribute.ProcessOrder == SlnSectionType.PreProcess) { + var sec = file.Sections.GetSection (secAttribute.SectionName, SlnSectionType.PreProcess); + if (sec != null) + sec.Properties.ReadObjectProperties (this); + } + next.OnReadSolution (monitor, file); + + if (secAttribute != null && secAttribute.ProcessOrder == SlnSectionType.PostProcess) { + var sec = file.Sections.GetSection (secAttribute.SectionName, SlnSectionType.PostProcess); + if (sec != null) + sec.Properties.ReadObjectProperties (this); + } } internal protected virtual void OnReadSolutionFolderItemData (ProgressMonitor monitor, SlnPropertySet properties, SolutionFolderItem item) @@ -103,7 +116,20 @@ namespace MonoDevelop.Projects internal protected virtual void OnWriteSolution (ProgressMonitor monitor, SlnFile file) { + var secAttribute = (SolutionDataSectionAttribute) Attribute.GetCustomAttribute (GetType(), typeof(SolutionDataSectionAttribute)); + if (secAttribute != null && secAttribute.ProcessOrder == SlnSectionType.PreProcess) { + var sec = file.Sections.GetOrCreateSection (secAttribute.SectionName, SlnSectionType.PreProcess); + sec.SkipIfEmpty = true; + sec.Properties.WriteObjectProperties (this); + } + next.OnWriteSolution (monitor, file); + + if (secAttribute != null && secAttribute.ProcessOrder == SlnSectionType.PostProcess) { + var sec = file.Sections.GetOrCreateSection (secAttribute.SectionName, SlnSectionType.PostProcess); + sec.SkipIfEmpty = true; + sec.Properties.WriteObjectProperties (this); + } } internal protected virtual void OnWriteSolutionFolderItemData (ProgressMonitor monitor, SlnPropertySet properties, SolutionFolderItem item) diff --git a/main/tests/UnitTests/MonoDevelop.Projects/PolicyTests.cs b/main/tests/UnitTests/MonoDevelop.Projects/PolicyTests.cs new file mode 100644 index 0000000000..3a5e23abf4 --- /dev/null +++ b/main/tests/UnitTests/MonoDevelop.Projects/PolicyTests.cs @@ -0,0 +1,37 @@ +// +// PolicyTests.cs +// +// Author: +// Lluis Sanchez Gual <lluis@xamarin.com> +// +// Copyright (c) 2015 Xamarin, Inc (http://www.xamarin.com) +// +// 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 NUnit.Framework; +using UnitTests; + +namespace MonoDevelop.Projects +{ + [TestFixture] + public class PolicyTests: TestBase + { + } +} + diff --git a/main/tests/UnitTests/MonoDevelop.Projects/SolutionTests.cs b/main/tests/UnitTests/MonoDevelop.Projects/SolutionTests.cs index 3f50885211..4b288fe6f4 100644 --- a/main/tests/UnitTests/MonoDevelop.Projects/SolutionTests.cs +++ b/main/tests/UnitTests/MonoDevelop.Projects/SolutionTests.cs @@ -35,6 +35,8 @@ using MonoDevelop.Core; using MonoDevelop.Projects.Formats.MSBuild; using MonoDevelop.Core.ProgressMonitoring; using System.Threading.Tasks; +using MonoDevelop.Core.Serialization; +using MonoDevelop.Projects.Extensions; namespace MonoDevelop.Projects { @@ -894,6 +896,103 @@ namespace MonoDevelop.Projects Assert.IsTrue (p.ItemDependencies.Contains (lib2Reloaded)); Assert.AreEqual (1, p.ItemDependencies.Count); } + + [Test] + public async Task WriteCustomData () + { + var en = new CustomSolutionItemNode<TestSolutionExtension> (); + WorkspaceObject.RegisterCustomExtension (en); + try { + string solFile = Util.GetSampleProject ("solution-custom-data", "custom-data.sln"); + + var sol = new Solution (); + var ext = sol.GetService<TestSolutionExtension> (); + Assert.NotNull (ext); + ext.Prop1 = "one"; + ext.Prop2 = "two"; + ext.Extra = new ComplexSolutionData { + Prop3 = "three", + Prop4 = "four" + }; + var savedFile = solFile + ".saved.sln"; + await sol.SaveAsync (savedFile, Util.GetMonitor ()); + Assert.AreEqual (File.ReadAllText (solFile), File.ReadAllText (savedFile)); + } finally { + WorkspaceObject.UnregisterCustomExtension (en); + } + } + + [Test] + public async Task ReadCustomData () + { + var en = new CustomSolutionItemNode<TestSolutionExtension> (); + WorkspaceObject.RegisterCustomExtension (en); + try { + string solFile = Util.GetSampleProject ("solution-custom-data", "custom-data.sln"); + var sol = (Solution) await Services.ProjectService.ReadWorkspaceItem (Util.GetMonitor (), solFile); + + var ext = sol.GetService<TestSolutionExtension> (); + Assert.NotNull (ext); + Assert.AreEqual ("one", ext.Prop1); + Assert.AreEqual ("two", ext.Prop2); + Assert.NotNull (ext.Extra); + Assert.AreEqual ("three", ext.Extra.Prop3); + Assert.AreEqual ("four", ext.Extra.Prop4); + } finally { + WorkspaceObject.UnregisterCustomExtension (en); + } + } + + [Test] + public async Task KeepUnknownCustomData () + { + var en = new CustomSolutionItemNode<TestSolutionExtension> (); + WorkspaceObject.RegisterCustomExtension (en); + try { + FilePath solFile = Util.GetSampleProject ("solution-custom-data", "custom-data-keep-unknown.sln"); + var sol = (Solution) await Services.ProjectService.ReadWorkspaceItem (Util.GetMonitor (), solFile); + + var ext = sol.GetService<TestSolutionExtension> (); + ext.Prop1 = "one-mod"; + ext.Prop2 = ""; + ext.Extra.Prop3 = "three-mod"; + ext.Extra.Prop4 = ""; + + var refFile = solFile.ParentDirectory.Combine ("custom-data-keep-unknown.sln.saved"); + + await sol.SaveAsync (Util.GetMonitor ()); + + Assert.AreEqual (File.ReadAllText (refFile), File.ReadAllText (sol.FileName)); + + } finally { + WorkspaceObject.UnregisterCustomExtension (en); + } + } + + [Test] + public async Task RemoveCustomData () + { + var en = new CustomSolutionItemNode<TestSolutionExtension> (); + WorkspaceObject.RegisterCustomExtension (en); + try { + FilePath solFile = Util.GetSampleProject ("solution-custom-data", "custom-data.sln"); + var sol = (Solution) await Services.ProjectService.ReadWorkspaceItem (Util.GetMonitor (), solFile); + + var ext = sol.GetService<TestSolutionExtension> (); + ext.Prop1 = "xx"; + ext.Prop2 = ""; + ext.Extra = null; + + var refFile = solFile.ParentDirectory.Combine ("no-custom-data.sln"); + + await sol.SaveAsync (Util.GetMonitor ()); + + Assert.AreEqual (File.ReadAllText (refFile), File.ReadAllText (sol.FileName)); + + } finally { + WorkspaceObject.UnregisterCustomExtension (en); + } + } } class SomeItem: SolutionItem @@ -931,4 +1030,34 @@ namespace MonoDevelop.Projects UnboundEvents++; } } + + class CustomSolutionItemNode<T>: ProjectModelExtensionNode where T:new() + { + public override object CreateInstance () + { + return new T (); + } + } + + [SolutionDataSection ("TestData")] + class TestSolutionExtension: SolutionExtension + { + [ItemProperty ("prop1", DefaultValue = "xx")] + public string Prop1 { get; set; } + + [ItemProperty ("prop2", DefaultValue = "")] + public string Prop2 { get; set; } + + [ItemProperty ("extra")] + public ComplexSolutionData Extra { get; set; } + } + + class ComplexSolutionData + { + [ItemProperty ("prop3")] + public string Prop3 { get; set; } + + [ItemProperty ("prop4", DefaultValue = "")] + public string Prop4 { get; set; } + } } diff --git a/main/tests/UnitTests/UnitTests.csproj b/main/tests/UnitTests/UnitTests.csproj index 194233df60..b9b03e3275 100644 --- a/main/tests/UnitTests/UnitTests.csproj +++ b/main/tests/UnitTests/UnitTests.csproj @@ -270,6 +270,7 @@ <Compile Include="MonoDevelop.Projects\StringTagTests.cs" /> <Compile Include="MonoDevelop.Ide.Templates\FileTemplateParserTests.cs" /> <Compile Include="MonoDevelop.Ide.Templates\ProjectCreateInformationTests.cs" /> + <Compile Include="MonoDevelop.Projects\PolicyTests.cs" /> </ItemGroup> <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" /> <Import Project="..\..\md.targets" /> |