aboutsummaryrefslogtreecommitdiff
path: root/src/WixToolset.Core/Link/WixGroupingOrdering.cs
diff options
context:
space:
mode:
Diffstat (limited to 'src/WixToolset.Core/Link/WixGroupingOrdering.cs')
-rw-r--r--src/WixToolset.Core/Link/WixGroupingOrdering.cs726
1 files changed, 726 insertions, 0 deletions
diff --git a/src/WixToolset.Core/Link/WixGroupingOrdering.cs b/src/WixToolset.Core/Link/WixGroupingOrdering.cs
new file mode 100644
index 00000000..fc0ce43b
--- /dev/null
+++ b/src/WixToolset.Core/Link/WixGroupingOrdering.cs
@@ -0,0 +1,726 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Link
4{
5 using System;
6 using System.Collections;
7 using System.Collections.ObjectModel;
8 using System.Collections.Generic;
9 using System.Diagnostics;
10 using System.Globalization;
11 using System.Linq;
12 using System.Text;
13 using WixToolset.Extensibility;
14 using WixToolset.Data;
15
16 /// <summary>
17 /// Grouping and Ordering class of the WiX toolset.
18 /// </summary>
19 internal sealed class WixGroupingOrdering : IMessageHandler
20 {
21 private Output output;
22 private IMessageHandler messageHandler;
23 private List<string> groupTypes;
24 private List<string> itemTypes;
25 private ItemCollection items;
26 private List<int> rowsUsed;
27 private bool loaded;
28 private bool encounteredError;
29
30 /// <summary>
31 /// Creates a WixGroupingOrdering object.
32 /// </summary>
33 /// <param name="output">Output from which to read the group and order information.</param>
34 /// <param name="messageHandler">Handler for any error messages.</param>
35 /// <param name="groupTypes">Group types to include.</param>
36 /// <param name="itemTypes">Item types to include.</param>
37 public WixGroupingOrdering(Output output, IMessageHandler messageHandler)
38 {
39 this.output = output;
40 this.messageHandler = messageHandler;
41
42 this.rowsUsed = new List<int>();
43 this.loaded = false;
44 this.encounteredError = false;
45 }
46
47 /// <summary>
48 /// Switches a WixGroupingOrdering object to operate on a new set of groups/items.
49 /// </summary>
50 /// <param name="groupTypes">Group types to include.</param>
51 /// <param name="itemTypes">Item types to include.</param>
52 public void UseTypes(IEnumerable<string> groupTypes, IEnumerable<string> itemTypes)
53 {
54 this.groupTypes = new List<string>(groupTypes);
55 this.itemTypes = new List<string>(itemTypes);
56
57 this.items = new ItemCollection();
58 this.loaded = false;
59 }
60
61 /// <summary>
62 /// Sends a message to the message handler if there is one.
63 /// </summary>
64 /// <param name="mea">Message event arguments.</param>
65 public void OnMessage(MessageEventArgs e)
66 {
67 WixErrorEventArgs errorEventArgs = e as WixErrorEventArgs;
68
69 if (null != errorEventArgs || MessageLevel.Error == e.Level)
70 {
71 this.encounteredError = true;
72 }
73
74 if (null != this.messageHandler)
75 {
76 this.messageHandler.OnMessage(e);
77 }
78 else if (null != errorEventArgs)
79 {
80 throw new WixException(errorEventArgs);
81 }
82 }
83
84 /// <summary>
85 /// Finds all nested items under a parent group and creates new WixGroup data for them.
86 /// </summary>
87 /// <param name="parentType">The group type for the parent group to flatten.</param>
88 /// <param name="parentId">The identifier of the parent group to flatten.</param>
89 /// <param name="removeUsedRows">Whether to remove used group rows before returning.</param>
90 public void FlattenAndRewriteRows(string parentType, string parentId, bool removeUsedRows)
91 {
92 Debug.Assert(this.groupTypes.Contains(parentType));
93
94 List<Item> orderedItems;
95 this.CreateOrderedList(parentType, parentId, out orderedItems);
96 if (this.encounteredError)
97 {
98 return;
99 }
100
101 this.CreateNewGroupRows(parentType, parentId, orderedItems);
102
103 if (removeUsedRows)
104 {
105 this.RemoveUsedGroupRows();
106 }
107 }
108
109 /// <summary>
110 /// Finds all items under a parent group type and creates new WixGroup data for them.
111 /// </summary>
112 /// <param name="parentType">The type of the parent group to flatten.</param>
113 /// <param name="removeUsedRows">Whether to remove used group rows before returning.</param>
114 public void FlattenAndRewriteGroups(string parentType, bool removeUsedRows)
115 {
116 Debug.Assert(this.groupTypes.Contains(parentType));
117
118 this.LoadFlattenOrderGroups();
119 if (this.encounteredError)
120 {
121 return;
122 }
123
124 foreach (Item item in this.items)
125 {
126 if (parentType == item.Type)
127 {
128 List<Item> orderedItems;
129 this.CreateOrderedList(item.Type, item.Id, out orderedItems);
130 this.CreateNewGroupRows(item.Type, item.Id, orderedItems);
131 }
132 }
133
134 if (removeUsedRows)
135 {
136 this.RemoveUsedGroupRows();
137 }
138 }
139
140
141 /// <summary>
142 /// Creates a flattened and ordered list of items for the given parent group.
143 /// </summary>
144 /// <param name="parentType">The group type for the parent group to flatten.</param>
145 /// <param name="parentId">The identifier of the parent group to flatten.</param>
146 /// <param name="orderedItems">The returned list of ordered items.</param>
147 private void CreateOrderedList(string parentType, string parentId, out List<Item> orderedItems)
148 {
149 orderedItems = null;
150
151 this.LoadFlattenOrderGroups();
152 if (this.encounteredError)
153 {
154 return;
155 }
156
157 Item parentItem;
158 if (!this.items.TryGetValue(parentType, parentId, out parentItem))
159 {
160 this.OnMessage(WixErrors.IdentifierNotFound(parentType, parentId));
161 return;
162 }
163
164 orderedItems = new List<Item>(parentItem.ChildItems);
165 orderedItems.Sort(new Item.AfterItemComparer());
166 }
167
168 /// <summary>
169 /// Removes rows from WixGroup that have been used by this object.
170 /// </summary>
171 public void RemoveUsedGroupRows()
172 {
173 List<int> sortedIndexes = this.rowsUsed.Distinct().OrderByDescending(i => i).ToList();
174
175 Table wixGroupTable = this.output.Tables["WixGroup"];
176 Debug.Assert(null != wixGroupTable);
177 Debug.Assert(sortedIndexes[0] < wixGroupTable.Rows.Count);
178
179 foreach (int rowIndex in sortedIndexes)
180 {
181 wixGroupTable.Rows.RemoveAt(rowIndex);
182 }
183 }
184
185 /// <summary>
186 /// Creates new WixGroup rows for a list of items.
187 /// </summary>
188 /// <param name="parentType">The group type for the parent group in the new rows.</param>
189 /// <param name="parentId">The identifier of the parent group in the new rows.</param>
190 /// <param name="orderedItems">The list of new items.</param>
191 private void CreateNewGroupRows(string parentType, string parentId, List<Item> orderedItems)
192 {
193 // TODO: MSIs don't guarantee that rows stay in the same order, and technically, neither
194 // does WiX (although they do, currently). We probably want to "upgrade" this to a new
195 // table that includes a sequence number, and then change the code that uses ordered
196 // groups to read from that table instead.
197 Table wixGroupTable = this.output.Tables["WixGroup"];
198 Debug.Assert(null != wixGroupTable);
199
200 foreach (Item item in orderedItems)
201 {
202 Row row = wixGroupTable.CreateRow(item.Row.SourceLineNumbers);
203 row[0] = parentId;
204 row[1] = parentType;
205 row[2] = item.Id;
206 row[3] = item.Type;
207 }
208 }
209
210 // Group/Ordering Flattening Logic
211 //
212 // What follows is potentially convoluted logic. Two somewhat orthogonal concepts are in
213 // play: grouping (parent/child relationships) and ordering (before/after relationships).
214 // Dealing with just one or the other is straghtforward. Groups can be flattened
215 // recursively. Ordering can be propagated in either direction. When the ordering also
216 // participates in the grouping constructions, however, things get trickier. For the
217 // purposes of this discussion, we're dealing with "items" and "groups", and an instance
218 // of either of them can be marked as coming "after" some other instance.
219 //
220 // For simple item-to-item ordering, the "after" values simply propagate: if A is after B,
221 // and B is after C, then we can say that A is after *both* B and C. If a group is involved,
222 // it acts as a proxy for all of its included items and any sub-groups.
223
224 /// <summary>
225 /// Internal workhorse for ensuring that group and ordering information has
226 /// been loaded and applied.
227 /// </summary>
228 private void LoadFlattenOrderGroups()
229 {
230 if (!this.loaded)
231 {
232 this.LoadGroups();
233 this.LoadOrdering();
234
235 // It would be really nice to have a "find circular after dependencies"
236 // function, but it gets much more complicated because of the way that
237 // the dependencies are propagated across group boundaries. For now, we
238 // just live with the dependency loop detection as we flatten the
239 // dependencies. Group references, however, we can check directly.
240 this.FindCircularGroupReferences();
241
242 if (!this.encounteredError)
243 {
244 this.FlattenGroups();
245 this.FlattenOrdering();
246 }
247
248 this.loaded = true;
249 }
250 }
251
252 /// <summary>
253 /// Loads data from the WixGroup table.
254 /// </summary>
255 private void LoadGroups()
256 {
257 Table wixGroupTable = this.output.Tables["WixGroup"];
258 if (null == wixGroupTable || 0 == wixGroupTable.Rows.Count)
259 {
260 // TODO: Change message name to make it *not* Bundle specific?
261 this.OnMessage(WixErrors.MissingBundleInformation("WixGroup"));
262 }
263
264 // Collect all of the groups
265 for (int rowIndex = 0; rowIndex < wixGroupTable.Rows.Count; ++rowIndex)
266 {
267 Row row = wixGroupTable.Rows[rowIndex];
268 string rowParentName = (string)row[0];
269 string rowParentType = (string)row[1];
270 string rowChildName = (string)row[2];
271 string rowChildType = (string)row[3];
272
273 // If this row specifies a parent or child type that's not in our
274 // lists, we assume it's not a row that we're concerned about.
275 if (!this.groupTypes.Contains(rowParentType) ||
276 !this.itemTypes.Contains(rowChildType))
277 {
278 continue;
279 }
280
281 this.rowsUsed.Add(rowIndex);
282
283 Item parentItem;
284 if (!this.items.TryGetValue(rowParentType, rowParentName, out parentItem))
285 {
286 parentItem = new Item(row, rowParentType, rowParentName);
287 this.items.Add(parentItem);
288 }
289
290 Item childItem;
291 if (!this.items.TryGetValue(rowChildType, rowChildName, out childItem))
292 {
293 childItem = new Item(row, rowChildType, rowChildName);
294 this.items.Add(childItem);
295 }
296
297 parentItem.ChildItems.Add(childItem);
298 }
299 }
300
301 /// <summary>
302 /// Flattens group/item information.
303 /// </summary>
304 private void FlattenGroups()
305 {
306 foreach (Item item in this.items)
307 {
308 item.FlattenChildItems();
309 }
310 }
311
312 /// <summary>
313 /// Finds and reports circular references in the group/item data.
314 /// </summary>
315 private void FindCircularGroupReferences()
316 {
317 ItemCollection itemsInKnownLoops = new ItemCollection();
318 foreach (Item item in this.items)
319 {
320 if (itemsInKnownLoops.Contains(item))
321 {
322 continue;
323 }
324
325 ItemCollection itemsSeen = new ItemCollection();
326 string circularReference;
327 if (this.FindCircularGroupReference(item, item, itemsSeen, out circularReference))
328 {
329 itemsInKnownLoops.Add(itemsSeen);
330 this.OnMessage(WixErrors.ReferenceLoopDetected(item.Row.SourceLineNumbers, circularReference));
331 }
332 }
333 }
334
335 /// <summary>
336 /// Recursive worker to find and report circular references in group/item data.
337 /// </summary>
338 /// <param name="checkItem">The sentinal item being checked.</param>
339 /// <param name="currentItem">The current item in the recursion.</param>
340 /// <param name="itemsSeen">A list of all items already visited (for performance).</param>
341 /// <param name="circularReference">A list of items in the current circular reference, if one was found; null otherwise.</param>
342 /// <returns>True if a circular reference was found; false otherwise.</returns>
343 private bool FindCircularGroupReference(Item checkItem, Item currentItem, ItemCollection itemsSeen, out string circularReference)
344 {
345 circularReference = null;
346 foreach (Item subitem in currentItem.ChildItems)
347 {
348 if (checkItem == subitem)
349 {
350 // TODO: Even better would be to include the source lines for each reference!
351 circularReference = String.Format(CultureInfo.InvariantCulture, "{0}:{1} -> {2}:{3}",
352 currentItem.Type, currentItem.Id, subitem.Type, subitem.Id);
353 return true;
354 }
355
356 if (!itemsSeen.Contains(subitem))
357 {
358 itemsSeen.Add(subitem);
359 if (this.FindCircularGroupReference(checkItem, subitem, itemsSeen, out circularReference))
360 {
361 // TODO: Even better would be to include the source lines for each reference!
362 circularReference = String.Format(CultureInfo.InvariantCulture, "{0}:{1} -> {2}",
363 currentItem.Type, currentItem.Id, circularReference);
364 return true;
365 }
366 }
367 }
368
369 return false;
370 }
371
372 /// <summary>
373 /// Loads ordering dependency data from the WixOrdering table.
374 /// </summary>
375 private void LoadOrdering()
376 {
377 Table wixOrderingTable = output.Tables["WixOrdering"];
378 if (null == wixOrderingTable || 0 == wixOrderingTable.Rows.Count)
379 {
380 // TODO: Do we need a message here?
381 return;
382 }
383
384 foreach (Row row in wixOrderingTable.Rows)
385 {
386 string rowItemType = (string)row[0];
387 string rowItemName = (string)row[1];
388 string rowDependsOnType = (string)row[2];
389 string rowDependsOnName = (string)row[3];
390
391 // If this row specifies some other (unknown) type in either
392 // position, we assume it's not a row that we're concerned about.
393 // For ordering, we allow group and item in either position.
394 if (!(this.groupTypes.Contains(rowItemType) || this.itemTypes.Contains(rowItemType)) ||
395 !(this.groupTypes.Contains(rowDependsOnType) || this.itemTypes.Contains(rowDependsOnType)))
396 {
397 continue;
398 }
399
400 Item item = null;
401 Item dependsOn = null;
402
403 if (!this.items.TryGetValue(rowItemType, rowItemName, out item))
404 {
405 this.OnMessage(WixErrors.IdentifierNotFound(rowItemType, rowItemName));
406 }
407
408 if (!this.items.TryGetValue(rowDependsOnType, rowDependsOnName, out dependsOn))
409 {
410 this.OnMessage(WixErrors.IdentifierNotFound(rowDependsOnType, rowDependsOnName));
411 }
412
413 if (null == item || null == dependsOn)
414 {
415 continue;
416 }
417
418 item.AddAfter(dependsOn, this);
419 }
420 }
421
422 /// <summary>
423 /// Flattens the ordering dependencies in the groups/items.
424 /// </summary>
425 private void FlattenOrdering()
426 {
427 // Because items don't know about their parent groups (and can, in fact, be
428 // in more than one group at a time), we need to pre-propagate the 'afters'
429 // from each parent item to its children before we attempt to flatten the
430 // ordering.
431 foreach (Item item in this.items)
432 {
433 item.PropagateAfterToChildItems(this);
434 }
435
436 foreach (Item item in this.items)
437 {
438 item.FlattenAfters(this);
439 }
440 }
441
442 /// <summary>
443 /// A variant of KeyedCollection that doesn't throw when an item is re-added.
444 /// </summary>
445 /// <typeparam name="TKey">Key type for the collection.</typeparam>
446 /// <typeparam name="TItem">Item type for the colelction.</typeparam>
447 internal abstract class EnhancedKeyCollection<TKey, TItem> : KeyedCollection<TKey, TItem>
448 {
449 new public void Add(TItem item)
450 {
451 if (!this.Contains(item))
452 {
453 base.Add(item);
454 }
455 }
456
457 public void Add(Collection<TItem> list)
458 {
459 foreach (TItem item in list)
460 {
461 this.Add(item);
462 }
463 }
464
465 public void Remove(Collection<TItem> list)
466 {
467 foreach (TItem item in list)
468 {
469 this.Remove(item);
470 }
471 }
472
473 public bool TryGetValue(TKey key, out TItem item)
474 {
475 // KeyedCollection doesn't implement the TryGetValue() method, but it's
476 // a useful concept. We can't just always pass this to the enclosed
477 // Dictionary, however, because it doesn't always exist! If it does, we
478 // can delegate to it as one would expect. If it doesn't, we have to
479 // implement everything ourselves in terms of Contains().
480
481 if (null != this.Dictionary)
482 {
483 return this.Dictionary.TryGetValue(key, out item);
484 }
485
486 if (this.Contains(key))
487 {
488 item = this[key];
489 return true;
490 }
491
492 item = default(TItem);
493 return false;
494 }
495
496#if DEBUG
497 // This just makes debugging easier...
498 public override string ToString()
499 {
500 StringBuilder sb = new StringBuilder();
501 foreach (TItem item in this)
502 {
503 sb.AppendFormat("{0}, ", item);
504 }
505 sb.Length -= 2;
506 return sb.ToString();
507 }
508#endif // DEBUG
509 }
510
511 /// <summary>
512 /// A specialized EnhancedKeyCollection, typed to Items.
513 /// </summary>
514 internal class ItemCollection : EnhancedKeyCollection<string, Item>
515 {
516 protected override string GetKeyForItem(Item item)
517 {
518 return item.Key;
519 }
520
521 public bool TryGetValue(string type, string id, out Item item)
522 {
523 return this.TryGetValue(CreateKeyFromTypeId(type, id), out item);
524 }
525
526 public static string CreateKeyFromTypeId(string type, string id)
527 {
528 return String.Format(CultureInfo.InvariantCulture, "{0}_{1}", type, id);
529 }
530 }
531
532 /// <summary>
533 /// An item (or group) in the grouping/ordering engine.
534 /// </summary>
535 /// <remarks>Encapsulates nested group membership and also before/after
536 /// ordering dependencies.</remarks>
537 internal class Item
538 {
539 private ItemCollection afterItems;
540 private ItemCollection beforeItems; // for checking for circular references
541 private bool flattenedAfterItems;
542
543 public Item(Row row, string type, string id)
544 {
545 this.Row = row;
546 this.Type = type;
547 this.Id = id;
548
549 this.Key = ItemCollection.CreateKeyFromTypeId(type, id);
550
551 afterItems = new ItemCollection();
552 beforeItems = new ItemCollection();
553 flattenedAfterItems = false;
554 }
555
556 public Row Row { get; private set; }
557 public string Type { get; private set; }
558 public string Id { get; private set; }
559 public string Key { get; private set; }
560
561#if DEBUG
562 // Makes debugging easier...
563 public override string ToString()
564 {
565 return this.Key;
566 }
567#endif // DEBUG
568
569 private ItemCollection childItems = new ItemCollection();
570 public ItemCollection ChildItems { get { return childItems; } }
571
572 /// <summary>
573 /// Removes any nested groups under this item and replaces
574 /// them with their child items.
575 /// </summary>
576 public void FlattenChildItems()
577 {
578 ItemCollection flattenedChildItems = new ItemCollection();
579
580 foreach (Item childItem in this.ChildItems)
581 {
582 if (0 == childItem.ChildItems.Count)
583 {
584 flattenedChildItems.Add(childItem);
585 }
586 else
587 {
588 childItem.FlattenChildItems();
589 flattenedChildItems.Add(childItem.ChildItems);
590 }
591 }
592
593 this.ChildItems.Clear();
594 this.ChildItems.Add(flattenedChildItems);
595 }
596
597 /// <summary>
598 /// Adds a list of items to the 'after' ordering collection.
599 /// </summary>
600 /// <param name="items">List of items to add.</param>
601 /// <param name="messageHandler">Message handler in case a circular ordering reference is found.</param>
602 public void AddAfter(ItemCollection items, IMessageHandler messageHandler)
603 {
604 foreach (Item item in items)
605 {
606 this.AddAfter(item, messageHandler);
607 }
608 }
609
610 /// <summary>
611 /// Adds an item to the 'after' ordering collection.
612 /// </summary>
613 /// <param name="item">Items to add.</param>
614 /// <param name="messageHandler">Message handler in case a circular ordering reference is found.</param>
615 public void AddAfter(Item after, IMessageHandler messageHandler)
616 {
617 if (this.beforeItems.Contains(after))
618 {
619 // We could try to chain this up (the way that group circular dependencies
620 // are reported), but since we're in the process of flattening, we may already
621 // have lost some distinction between authored and propagated ordering.
622 string circularReference = String.Format(CultureInfo.InvariantCulture, "{0}:{1} -> {2}:{3} -> {0}:{1}",
623 this.Type, this.Id, after.Type, after.Id);
624 messageHandler.OnMessage(WixErrors.OrderingReferenceLoopDetected(after.Row.SourceLineNumbers, circularReference));
625 return;
626 }
627
628 this.afterItems.Add(after);
629 after.beforeItems.Add(this);
630 }
631
632 /// <summary>
633 /// Propagates 'after' dependencies from an item to its child items.
634 /// </summary>
635 /// <param name="messageHandler">Message handler in case a circular ordering reference is found.</param>
636 /// <remarks>Because items don't know about their parent groups (and can, in fact, be in more
637 /// than one group at a time), we need to propagate the 'afters' from each parent item to its children
638 /// before we attempt to flatten the ordering.</remarks>
639 public void PropagateAfterToChildItems(IMessageHandler messageHandler)
640 {
641 if (this.ShouldItemPropagateChildOrdering())
642 {
643 foreach (Item childItem in this.childItems)
644 {
645 childItem.AddAfter(this.afterItems, messageHandler);
646 }
647 }
648 }
649
650 /// <summary>
651 /// Flattens the ordering dependency for this item.
652 /// </summary>
653 /// <param name="messageHandler">Message handler in case a circular ordering reference is found.</param>
654 public void FlattenAfters(IMessageHandler messageHandler)
655 {
656 if (this.flattenedAfterItems)
657 {
658 return;
659 }
660
661 this.flattenedAfterItems = true;
662
663 // Ensure that if we're after something (A), and *it's* after something (B),
664 // that we list ourselved as after both (A) *and* (B).
665 ItemCollection nestedAfterItems = new ItemCollection();
666
667 foreach (Item afterItem in this.afterItems)
668 {
669 afterItem.FlattenAfters(messageHandler);
670 nestedAfterItems.Add(afterItem.afterItems);
671
672 if (afterItem.ShouldItemPropagateChildOrdering())
673 {
674 // If we are after a group, it really means
675 // we are after all of the group's children.
676 foreach (Item childItem in afterItem.ChildItems)
677 {
678 childItem.FlattenAfters(messageHandler);
679 nestedAfterItems.Add(childItem.afterItems);
680 nestedAfterItems.Add(childItem);
681 }
682 }
683 }
684
685 this.AddAfter(nestedAfterItems, messageHandler);
686 }
687
688 // We *don't* propagate ordering information from Packages or
689 // Containers to their children, because ordering doesn't matter
690 // for them, and a Payload in two Packages (or Containers) can
691 // cause a circular reference to occur. We do, however, need to
692 // track the ordering in the UX Container, because we need the
693 // first payload to be the entrypoint.
694 private bool ShouldItemPropagateChildOrdering()
695 {
696 if (String.Equals("Package", this.Type, StringComparison.Ordinal) ||
697 (String.Equals("Container", this.Type, StringComparison.Ordinal) &&
698 !String.Equals(Compiler.BurnUXContainerId, this.Id, StringComparison.Ordinal)))
699 {
700 return false;
701 }
702 return true;
703 }
704
705 /// <summary>
706 /// Helper IComparer class to make ordering easier.
707 /// </summary>
708 internal sealed class AfterItemComparer : IComparer<Item>
709 {
710 public int Compare(Item x, Item y)
711 {
712 if (x.afterItems.Contains(y))
713 {
714 return 1;
715 }
716 else if (y.afterItems.Contains(x))
717 {
718 return -1;
719 }
720
721 return string.CompareOrdinal(x.Id, y.Id);
722 }
723 }
724 }
725 }
726}