1 // Copyright (c) Microsoft. All rights reserved.
2 // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3 
4 using System;
5 using System.Collections.Generic;
6 using System.Text;
7 using Microsoft.Build.BuildEngine.Shared;
8 using System.Xml;
9 using System.Collections;
10 
11 namespace Microsoft.Build.BuildEngine
12 {
13     /// <summary>
14     /// Encapsulates, as far as possible, any XML behind the child of a BuildItemGroup element
15     /// </summary>
16     internal class BuildItemGroupChildXml
17     {
18         #region Fields
19 
20         // The element itself
21         private XmlElement element;
22         // The Condition attribute on it, if any, to save lookup
23         private XmlAttribute conditionAttribute;
24         // The Include attribute on it, if any, to save lookup
25         private XmlAttribute includeAttribute;
26         // The Exclude attribute on it, if any, to save lookup
27         private XmlAttribute excludeAttribute;
28         // The Remove attribute on it, if any, to save lookup
29         private XmlAttribute removeAttribute;
30         // Whether this is represents an add, remove, or modify
31         private ChildType childType;
32 
33         #endregion
34 
35         #region Constructors
36 
BuildItemGroupChildXml(XmlDocument ownerDocument, string name, string include)37         internal BuildItemGroupChildXml(XmlDocument ownerDocument, string name, string include)
38         {
39             this.element = ownerDocument.CreateElement(name, XMakeAttributes.defaultXmlNamespace);
40             this.Include = include;
41 
42         }
43 
BuildItemGroupChildXml(XmlElement element, ChildType childTypeExpected)44         internal BuildItemGroupChildXml(XmlElement element, ChildType childTypeExpected)
45         {
46             ErrorUtilities.VerifyThrow(element != null, "Need an XML node.");
47             ErrorUtilities.VerifyThrowNoAssert(childTypeExpected != ChildType.Invalid, "Can't expect invalid childtype");
48             ProjectXmlUtilities.VerifyThrowProjectValidNameAndNamespace(element);
49 
50             this.element = element;
51 
52             // Loop through each of the attributes on the item element.
53             foreach (XmlAttribute attribute in element.Attributes)
54             {
55                 switch (attribute.Name)
56                 {
57                     case XMakeAttributes.include:
58                         this.includeAttribute = attribute;
59                         break;
60 
61                     case XMakeAttributes.exclude:
62                         this.excludeAttribute = attribute;
63                         break;
64 
65                     case XMakeAttributes.condition:
66                         this.conditionAttribute = attribute;
67                         break;
68 
69                     case XMakeAttributes.xmlns:
70                         // We already verified that the namespace is correct
71                         break;
72 
73                     case XMakeAttributes.remove:
74                         this.removeAttribute = attribute;
75                         break;
76 
77                     case XMakeAttributes.keepMetadata:
78                     case XMakeAttributes.removeMetadata:
79                     case XMakeAttributes.keepDuplicates:
80                         // Ignore these - they are part of the new OM.
81                         break;
82 
83                     default:
84                         ProjectXmlUtilities.ThrowProjectInvalidAttribute(attribute);
85                         break;
86                 }
87             }
88 
89             this.childType = ChildType.Invalid;
90 
91             // Default to modify, if that's one of the child types we are told to expect.
92             if ((childTypeExpected & ChildType.BuildItemModify) == ChildType.BuildItemModify)
93             {
94                 this.childType = ChildType.BuildItemModify;
95             }
96 
97             if (this.includeAttribute != null)
98             {
99                 ProjectXmlUtilities.VerifyThrowProjectInvalidAttribute((childTypeExpected & ChildType.BuildItemAdd) == ChildType.BuildItemAdd, includeAttribute);
100                 ProjectErrorUtilities.VerifyThrowInvalidProject(Include.Length > 0, element, "MissingRequiredAttribute", XMakeAttributes.include, element.Name);
101                 ProjectXmlUtilities.VerifyThrowProjectInvalidAttribute(removeAttribute == null, removeAttribute);
102                 this.childType = ChildType.BuildItemAdd;
103             }
104 
105             if (this.excludeAttribute != null)
106             {
107                 ProjectXmlUtilities.VerifyThrowProjectInvalidAttribute((childTypeExpected & ChildType.BuildItemAdd) == ChildType.BuildItemAdd, excludeAttribute);
108                 ProjectErrorUtilities.VerifyThrowInvalidProject(Include.Length > 0, element, "MissingRequiredAttribute", XMakeAttributes.include, element.Name);
109                 ProjectXmlUtilities.VerifyThrowProjectInvalidAttribute(removeAttribute == null, removeAttribute);
110                 this.childType = ChildType.BuildItemAdd;
111             }
112 
113             if (this.removeAttribute != null)
114             {
115                 ProjectXmlUtilities.VerifyThrowProjectInvalidAttribute((childTypeExpected & ChildType.BuildItemRemove) == ChildType.BuildItemRemove, removeAttribute);
116                 ProjectErrorUtilities.VerifyThrowInvalidProject(Remove.Length > 0, element, "MissingRequiredAttribute", XMakeAttributes.remove, element.Name);
117                 ProjectXmlUtilities.VerifyThrowProjectInvalidAttribute(includeAttribute == null, includeAttribute);
118                 ProjectXmlUtilities.VerifyThrowProjectInvalidAttribute(excludeAttribute == null, excludeAttribute);
119                 this.childType = ChildType.BuildItemRemove;
120             }
121 
122             if (this.childType == ChildType.Invalid)
123             {
124                 // So the xml wasn't consistent with any of the child types that we were told to expect.
125                 // Figure out the most reasonable message to produce.
126                 if ((childTypeExpected & ChildType.BuildItemAdd) == ChildType.BuildItemAdd)
127                 {
128                     ProjectErrorUtilities.VerifyThrowInvalidProject(Include.Length > 0, element, "MissingRequiredAttribute", XMakeAttributes.include, element.Name);
129                 }
130                 else if ((childTypeExpected & ChildType.BuildItemRemove) == ChildType.BuildItemRemove)
131                 {
132                     ProjectErrorUtilities.VerifyThrowInvalidProject(Remove.Length > 0, element, "MissingRequiredAttribute", XMakeAttributes.remove, element.Name);
133                 }
134                 else
135                 {
136                     ErrorUtilities.ThrowInternalError("Unexpected child type");
137                 }
138             }
139 
140             // Validate each of the child nodes beneath the item.
141             List<XmlElement> children = ProjectXmlUtilities.GetValidChildElements(element);
142 
143             if (this.childType == ChildType.BuildItemRemove && children.Count != 0)
144             {
145                 ProjectErrorUtilities.ThrowInvalidProject(element, "ChildElementsBelowRemoveNotAllowed", children[0].Name);
146             }
147 
148             foreach (XmlElement child in children)
149             {
150                 ProjectXmlUtilities.VerifyThrowProjectValidNameAndNamespace(child);
151 
152                 ProjectErrorUtilities.VerifyThrowInvalidProject(!FileUtilities.IsItemSpecModifier(child.Name), child, "ItemSpecModifierCannotBeCustomMetadata", child.Name);
153                 ProjectErrorUtilities.VerifyThrowInvalidProject(XMakeElements.IllegalItemPropertyNames[child.Name] == null, child, "CannotModifyReservedItemMetadata", child.Name);
154             }
155         }
156 
157         #endregion
158 
159 
160         #region Properties
161 
162         internal string Name
163         {
164             get
165             {
166                 return element.Name;
167             }
168 
169             set
170             {
171                 element = XmlUtilities.RenameXmlElement(element, value, XMakeAttributes.defaultXmlNamespace);
172 
173                 // Because this actually created a new element, we have to find the attributes again
174                 includeAttribute = element.Attributes[XMakeAttributes.include];
175                 excludeAttribute = element.Attributes[XMakeAttributes.exclude];
176                 conditionAttribute = element.Attributes[XMakeAttributes.condition];
177                 removeAttribute = element.Attributes[XMakeAttributes.remove];
178             }
179         }
180 
181         internal string Include
182         {
183             get
184             {
185                 return ProjectXmlUtilities.GetAttributeValue(includeAttribute);
186             }
187 
188             set
189             {
190                 element.SetAttribute(XMakeAttributes.include, value);
191                 includeAttribute = element.Attributes[XMakeAttributes.include];
192             }
193         }
194 
195         internal string Exclude
196         {
197             get
198             {
199                 return ProjectXmlUtilities.GetAttributeValue(excludeAttribute);
200             }
201 
202             set
203             {
204                 excludeAttribute = ProjectXmlUtilities.SetOrRemoveAttribute(element, XMakeAttributes.exclude, value);
205             }
206         }
207 
208         internal string Remove
209         {
210             get
211             {
212                 return ProjectXmlUtilities.GetAttributeValue(removeAttribute);
213             }
214         }
215 
216         internal string Condition
217         {
218             get
219             {
220                 return ProjectXmlUtilities.GetAttributeValue(conditionAttribute);
221             }
222 
223             set
224             {
225                 conditionAttribute = ProjectXmlUtilities.SetOrRemoveAttribute(element, XMakeAttributes.condition, value);
226             }
227         }
228 
229         internal XmlElement Element
230         {
231             get { return element; }
232         }
233 
234         internal XmlAttribute IncludeAttribute
235         {
236             get { return includeAttribute; }
237         }
238 
239         internal XmlAttribute ExcludeAttribute
240         {
241             get { return excludeAttribute; }
242         }
243 
244         internal XmlAttribute RemoveAttribute
245         {
246             get { return removeAttribute; }
247         }
248 
249         internal XmlAttribute ConditionAttribute
250         {
251             get { return conditionAttribute; }
252         }
253 
254         internal ChildType ChildType
255         {
256             get { return childType; }
257         }
258 
259         #endregion
260 
261         #region Methods
262 
263         /// <summary>
264         /// Gets all child elements, ignoring whitespace and comments, and any conditions
265         /// </summary>
GetChildren()266         internal List<XmlElement> GetChildren()
267         {
268             List<XmlElement> children = ProjectXmlUtilities.GetValidChildElements(element);
269             return children;
270         }
271 
272         /// <summary>
273         /// Removes all child elements with the specified name.
274         /// </summary>
RemoveChildrenByName(string name)275         internal void RemoveChildrenByName(string name)
276         {
277             List<XmlElement> children = GetChildren();
278             foreach (XmlElement child in children)
279             {
280                 if (String.Equals(name, child.Name, StringComparison.OrdinalIgnoreCase))
281                 {
282                     element.RemoveChild(child);
283                 }
284             }
285         }
286 
287         /// <summary>
288         /// Ensures there's a child element with the specified name and value.
289         /// Disregards any Condition attributes on the children.
290         /// If several children are already present with the specified name, removes all except the last one.
291         /// If a child is present with the specified name, does not modify it if the value is already as specified.
292         /// Returns true if the XML was meaningfully modified.
293         /// </summary>
SetChildValue(string name, string value)294         internal bool SetChildValue(string name, string value)
295         {
296             bool dirty = false;
297             XmlElement childToModify = null;
298             List<XmlElement> children = GetChildren();
299 
300             foreach (XmlElement child in children)
301             {
302                 if (String.Equals(name, child.Name, StringComparison.OrdinalIgnoreCase))
303                 {
304                     if (childToModify != null)
305                     {
306                         // We already found a matching child, remove that, we'll
307                         // use this later one; the later one was winning anyway,
308                         // so we don't consider this dirtying the item
309                         element.RemoveChild(childToModify);
310                     }
311 
312                     childToModify = child;
313                 }
314             }
315 
316             if (childToModify == null)
317             {
318                 // create a new child as specified
319                 childToModify = element.OwnerDocument.CreateElement(name, XMakeAttributes.defaultXmlNamespace);
320                 element.AppendChild(childToModify);
321                 dirty = true;
322             }
323 
324             // if we just added this child, or the old value and new value are different...
325             if (dirty || !String.Equals(Utilities.GetXmlNodeInnerContents(childToModify), value, StringComparison.Ordinal))
326             {
327                 // give the child the new value
328                 Utilities.SetXmlNodeInnerContents(childToModify, value);
329                 dirty = true;
330             }
331 
332             return dirty;
333         }
334 
335         #endregion
336     }
337 
338     /// <summary>
339     /// Type of the item group child element
340     /// </summary>
341     internal enum ChildType
342     {
343         Invalid = 0,
344 
345         /// <summary>
346         /// Regular item, with Include and possibly Exclude attributes
347         /// </summary>
348         BuildItemAdd = 1,
349 
350         /// <summary>
351         /// Remove item, with Remove attribute
352         /// </summary>
353         BuildItemRemove = 2,
354 
355         /// <summary>
356         /// Modify item, with no attributes (except possibly Condition)
357         /// </summary>
358         BuildItemModify = 4,
359 
360         /// <summary>
361         /// Add, remove, or modify item expression
362         /// </summary>
363         Any = BuildItemAdd | BuildItemRemove | BuildItemModify
364     }
365 
366 }
367