LotSizingDataModel.Core 2.0.1
Core domain model, shared abstractions and XML-serializable entities.
Loading...
Searching...
No Matches
TransportXmlMigration.cs
Go to the documentation of this file.
1using System;
2using System.Globalization;
3using System.Linq;
4using System.Xml;
5using System.Xml.Linq;
7
9
10/// <summary>Lossless, in-memory migration of legacy nested transport lanes. Input files are never written.</summary>
11public static class TransportXmlMigration
12{
13 /// <summary>Loads XML through the caller's secure reader and returns a migrated copy.</summary>
14 public static XDocument ReadAndMigrate(XmlReader reader)
15 {
16 ArgumentNullException.ThrowIfNull(reader);
17 var document = XDocument.Load(reader);
18 foreach (var chain in document.Descendants().Where(e => e.Name.LocalName == "supplyChain"))
19 MigrateSupplyChain(chain);
20 return document;
21 }
22
23 private static void MigrateSupplyChain(XElement chain)
24 {
25 XNamespace ns = chain.Name.Namespace;
26 var version = (string?)chain.Attribute("transportFormatVersion");
27 if (version is not null && version != "1" && version != "2")
28 throw new InvalidOperationException($"Unsupported transport format version '{version}'.");
29 var resources = chain.Element(ns + "transportResources")?.Elements(ns + "transportResource").ToArray() ?? [];
30 var containers = resources.SelectMany(r => r.Elements(ns + "lanes")).ToArray();
31 bool central = chain.Element(ns + "transportLanes") is not null || chain.Element(ns + "transportAssignments") is not null;
32 if (chain.Element(ns + "transportLanes")?.Elements(ns + "transportLane").Any(l => l.Attribute("leadTime") is not null) == true)
33 throw new InvalidOperationException("Central lanes cannot carry delays; put the delay on each assignment.");
34 if (containers.Length == 0)
35 {
36 if (version == "1" && central)
37 throw new InvalidOperationException("Version 1 cannot contain central transport data.");
38 if (version == "1") chain.SetAttributeValue("transportFormatVersion", "2");
39 return;
40 }
41 if (central || version == "2")
42 throw new InvalidOperationException("Mixed legacy and central transport representations cannot be merged implicitly.");
43
44 var entries = resources.SelectMany(resource => resource.Elements(ns + "lanes")
45 .SelectMany(c => c.Elements()).Select(lane =>
46 {
47 if (lane.Name != ns + "transportLane" || lane.Attributes().Any(a => a.Name != "leadTime") ||
48 lane.Elements().Any(e => e.Name != ns + "origin" && e.Name != ns + "destination"))
49 throw new InvalidOperationException("Unrecognized legacy lane data; migration would lose information.");
50 var origin = ReadWarehouse(lane, ns + "origin");
51 var destination = ReadWarehouse(lane, ns + "destination");
52 if (origin.Key == destination.Key) throw new InvalidOperationException("A legacy lane connects a warehouse to itself.");
53 int delay = ReadInteger(lane, "leadTime");
54 int resourceId = ReadInteger(resource, "id");
55 if (delay < 0 || resourceId < 0) throw new InvalidOperationException("Negative legacy delay or resource identifier.");
56 return new { ResourceId = resourceId, Delay = delay, Origin = origin.Element, Destination = destination.Element,
57 Key = (origin.Key.Kind, origin.Key.Id, destination.Key.Kind, destination.Key.Id) };
58 })).ToArray();
59
60 var lanes = new XElement(ns + "transportLanes");
61 var assignments = new XElement(ns + "transportAssignments");
62 int id = 0;
63 // Numeric tuple ordering is culture-independent and independent of resource/input ordering.
64 foreach (var group in entries.GroupBy(e => e.Key).OrderBy(g => g.Key))
65 {
66 id = checked(id + 1);
67 var first = group.First();
68 lanes.Add(new XElement(ns + "transportLane", new XAttribute("id", id),
69 new XElement(first.Origin), new XElement(first.Destination)));
70 foreach (var pair in group.GroupBy(e => e.ResourceId).OrderBy(g => g.Key))
71 {
72 int[] delays = pair.Select(e => e.Delay).Distinct().ToArray();
73 if (delays.Length != 1)
74 throw new InvalidOperationException($"Conflicting legacy delays for lane {group.Key}, resource {pair.Key}.");
75 assignments.Add(new XElement(ns + "transportAssignment", new XAttribute("laneId", id),
76 new XAttribute("transportResourceId", pair.Key), new XAttribute("leadTime", delays[0])));
77 }
78 }
79 // Commit only after every pair has passed conflict detection.
80 foreach (var container in containers) container.Remove();
81 chain.Add(lanes, assignments);
82 chain.SetAttributeValue("transportFormatVersion", "2");
83 }
84
85 private static ((int Kind, int Id) Key, XElement Element) ReadWarehouse(XElement lane, XName name)
86 {
87 var element = lane.Elements(name).SingleOrDefault()
88 ?? throw new InvalidOperationException($"Missing legacy {name.LocalName}.");
89 string? kindText = (string?)element.Attribute("kind");
90 if (!Enum.TryParse<WarehouseReferenceKind>(kindText, ignoreCase: true, out var kind) || !Enum.IsDefined(kind))
91 throw new InvalidOperationException($"Invalid warehouse kind '{kindText}'.");
92 int id = ReadInteger(element, "id");
93 if (id < 0) throw new InvalidOperationException("Negative warehouse identifier.");
94 return (((int)kind, id), element);
95 }
96
97 private static int ReadInteger(XElement element, string name) =>
98 int.TryParse((string?)element.Attribute(name), NumberStyles.Integer, CultureInfo.InvariantCulture, out int value)
99 ? value : throw new InvalidOperationException($"Missing or invalid integer '{name}' in legacy transport data.");
100}
Lossless, in-memory migration of legacy nested transport lanes. Input files are never written.
static XDocument ReadAndMigrate(XmlReader reader)
Loads XML through the caller's secure reader and returns a migrated copy.
WarehouseReferenceKind
Identifies the type of warehouse referenced by a relationship.