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 // </copyright>
5 // <summary>Represents a set of evaluated item definitions all applying to the same item-type.</summary>
6 //-----------------------------------------------------------------------
7 
8 using System.Diagnostics;
9 using System.Diagnostics.CodeAnalysis;
10 using Microsoft.Build.Collections;
11 using Microsoft.Build.Construction;
12 using Microsoft.Build.Execution;
13 using Microsoft.Build.Shared;
14 using System.Collections.Generic;
15 using System;
16 using System.Linq;
17 
18 namespace Microsoft.Build.Evaluation
19 {
20     /// <summary>
21     /// An evaluated item definition for a particular item-type.
22     /// </summary>
23     /// <remarks>
24     /// Note that these are somewhat different to items. Like items, they can have metadata; like properties, the metadata
25     /// can override each other. So during evaluation all the item definitions for a type are rolled together (assuming
26     /// their conditions are true) to create one ProjectItemDefinition for each type. For this reason, the ProjectItemDefinition
27     /// often will not point to a single ProjectItemDefinitionElement. The metadata within, however, will each point to a single
28     /// ProjectMetadataElement, and these can be added, removed, and modified.
29     /// </remarks>
30     [DebuggerDisplay("{_itemType} #Metadata={MetadataCount}")]
31     public class ProjectItemDefinition : IKeyed, IMetadataTable, IItemDefinition<ProjectMetadata>, IProjectMetadataParent
32     {
33         /// <summary>
34         /// Project that this item definition lives in.
35         /// ProjectItemDefinitions always live in a project.
36         /// Used to evaluate any updates to child metadata.
37         /// </summary>
38         private readonly Project _project;
39 
40         /// <summary>
41         /// Item type, for example "Compile", that this item definition applies to
42         /// </summary>
43         private readonly string _itemType;
44 
45         /// <summary>
46         /// Collection of metadata that link the XML metadata and instance metadata
47         /// Since evaluation has occurred, this is an unordered collection.
48         /// </summary>
49         private PropertyDictionary<ProjectMetadata> _metadata;
50 
51         /// <summary>
52         /// Called by the Evaluator during project evaluation.
53         /// </summary>
54         /// <remarks>
55         /// Assumes that the itemType string originated in a ProjectItemDefinitionElement and therefore
56         /// was already validated.
57         /// </remarks>
ProjectItemDefinition(Project project, string itemType)58         internal ProjectItemDefinition(Project project, string itemType)
59         {
60             ErrorUtilities.VerifyThrowInternalNull(project, "project");
61             ErrorUtilities.VerifyThrowArgumentLength(itemType, "itemType");
62 
63             _project = project;
64             _itemType = itemType;
65             _metadata = null;
66         }
67 
68         /// <summary>
69         /// Project that this item lives in.
70         /// ProjectDefinitions always live in a project.
71         /// </summary>
72         [DebuggerBrowsable(DebuggerBrowsableState.Never)]
73         public Project Project
74         {
75             [DebuggerStepThrough]
76             get
77             { return _project; }
78         }
79 
80         /// <summary>
81         /// Type of this item definition.
82         /// </summary>
83         [DebuggerBrowsable(DebuggerBrowsableState.Never)]
84         public string ItemType
85         {
86             [DebuggerStepThrough]
87             get
88             { return _itemType; }
89         }
90 
91         /// <summary>
92         /// Metadata on the item definition.
93         /// If there is no metadata, returns empty collection.
94         /// This is a read-only collection.
95         /// </summary>
96         [SuppressMessage("Microsoft.Naming", "CA1721:PropertyNamesShouldNotMatchGetMethods", Justification = "This is a reasonable choice. API review approved")]
97         public IEnumerable<ProjectMetadata> Metadata => _metadata ?? Enumerable.Empty<ProjectMetadata>();
98 
99         /// <summary>
100         /// Count of metadata on the item definition.
101         /// </summary>
102         public int MetadataCount
103         {
104             get { return (_metadata == null) ? 0 : _metadata.Count; }
105         }
106 
107         /// <summary>
108         /// Implementation of IKeyed exposing the item type, so these
109         /// can be put in a dictionary conveniently.
110         /// </summary>
111         string IKeyed.Key
112         {
113             get { return ItemType; }
114         }
115 
116         /// <summary>
117         /// Get any metadata in the item that has the specified name,
118         /// otherwise returns null
119         /// </summary>
120         [DebuggerStepThrough]
GetMetadata(string name)121         public ProjectMetadata GetMetadata(string name)
122         {
123             return (_metadata == null) ? null : _metadata[name];
124         }
125 
126         /// <summary>
127         /// Get the value of any metadata in the item that has the specified
128         /// name, otherwise returns null
129         /// </summary>
GetMetadataValue(string name)130         public string GetMetadataValue(string name)
131         {
132             string escapedValue = (this as IMetadataTable).GetEscapedValue(name);
133 
134             return (escapedValue == null) ? null : EscapingUtilities.UnescapeAll(escapedValue);
135         }
136 
137         /// <summary>
138         /// Sets a new metadata value on the ItemDefinition.
139         /// </summary>
140         /// <remarks>Unevaluated value is assumed to be escaped as necessary</remarks>
SetMetadataValue(string name, string unevaluatedValue)141         public ProjectMetadata SetMetadataValue(string name, string unevaluatedValue)
142         {
143             XmlUtilities.VerifyThrowArgumentValidElementName(name);
144             ErrorUtilities.VerifyThrowArgument(!FileUtilities.ItemSpecModifiers.IsItemSpecModifier(name), "ItemSpecModifierCannotBeCustomMetadata", name);
145             ErrorUtilities.VerifyThrowInvalidOperation(XMakeElements.IllegalItemPropertyNames[name] == null, "CannotModifyReservedItemMetadata", name);
146 
147             ProjectMetadata metadatum;
148 
149             if (_metadata != null)
150             {
151                 metadatum = _metadata[name];
152 
153                 if (metadatum != null)
154                 {
155                     Project.VerifyThrowInvalidOperationNotImported(metadatum.Xml.ContainingProject);
156                     metadatum.UnevaluatedValue = unevaluatedValue;
157                     return metadatum;
158                 }
159             }
160 
161             // We can't use the item definition that this object came from as a root, as it doesn't map directly
162             // to a single XML element. Instead, add a new one to the project. Best we can do.
163             ProjectItemDefinitionElement itemDefinition = _project.Xml.AddItemDefinition(_itemType);
164 
165             ProjectMetadataElement metadatumXml = itemDefinition.AddMetadata(name, unevaluatedValue);
166 
167             _metadata = _metadata ?? new PropertyDictionary<ProjectMetadata>();
168 
169             string evaluatedValueEscaped = _project.ExpandMetadataValueBestEffortLeaveEscaped(this, unevaluatedValue, metadatumXml.Location);
170 
171             metadatum = new ProjectMetadata(this, metadatumXml, evaluatedValueEscaped, null /* predecessor unknown */);
172 
173             _metadata.Set(metadatum);
174 
175             return metadatum;
176         }
177 
178         #region IItemDefinition Members
179 
180         /// <summary>
181         /// Sets a new metadata value on the ItemDefinition.
182         /// This is ONLY called during evaluation and does not affect the XML.
183         /// </summary>
SetMetadata(ProjectMetadataElement metadataElement, string evaluatedValue, ProjectMetadata predecessor)184         ProjectMetadata IItemDefinition<ProjectMetadata>.SetMetadata(ProjectMetadataElement metadataElement, string evaluatedValue, ProjectMetadata predecessor)
185         {
186             _metadata = _metadata ?? new PropertyDictionary<ProjectMetadata>();
187 
188             ProjectMetadata metadatum = new ProjectMetadata(this, metadataElement, evaluatedValue, predecessor);
189             _metadata.Set(metadatum);
190 
191             return metadatum;
192         }
193 
194         #endregion
195 
196         #region IMetadataTable Members
197 
198         /// <summary>
199         /// Retrieves the value of the named metadatum.
200         /// </summary>
201         /// <param name="name">The metadatum to retrieve.</param>
202         /// <returns>The value, or an empty string if there is none by that name.</returns>
IMetadataTable.GetEscapedValue(string name)203         string IMetadataTable.GetEscapedValue(string name)
204         {
205             return ((IMetadataTable)this).GetEscapedValue(null, name);
206         }
207 
208         /// <summary>
209         /// Retrieves the value of the named metadatum.
210         /// </summary>
211         /// <param name="specifiedItemType">The type of item.</param>
212         /// <param name="name">The metadatum to retrieve.</param>
213         /// <returns>The value, or an empty string if there is none by that name.</returns>
IMetadataTable.GetEscapedValue(string specifiedItemType, string name)214         string IMetadataTable.GetEscapedValue(string specifiedItemType, string name)
215         {
216             return ((IMetadataTable)this).GetEscapedValueIfPresent(specifiedItemType, name) ?? String.Empty;
217         }
218 
219         /// <summary>
220         /// Retrieves the value of the named metadatum, or null if it doesn't exist
221         /// </summary>
222         /// <param name="specifiedItemType">The type of item.</param>
223         /// <param name="name">The metadatum to retrieve.</param>
224         /// <returns>The value, or null if there is none by that name.</returns>
IMetadataTable.GetEscapedValueIfPresent(string specifiedItemType, string name)225         string IMetadataTable.GetEscapedValueIfPresent(string specifiedItemType, string name)
226         {
227             if (_metadata == null)
228             {
229                 return null;
230             }
231 
232             if (specifiedItemType == null || String.Equals(_itemType, specifiedItemType, StringComparison.OrdinalIgnoreCase))
233             {
234                 ProjectMetadata metadatum = GetMetadata(name);
235                 if (metadatum != null)
236                 {
237                     return metadatum.EvaluatedValueEscaped;
238                 }
239             }
240 
241             return null;
242         }
243 
244         #endregion
245     }
246 }
247