NanoXLSX.Compatibility 3.2.0
Loading...
Searching...
No Matches
ExternalLinkReader.cs
1/*
2 * NanoXLSX is a small .NET library to generate and read XLSX (Microsoft Excel 2007 or newer) files in an easy and native way
3 * Copyright Raphael Stoeckli © 2026
4 * This library is licensed under the MIT License.
5 * You find a copy of the license in project folder or on: http://opensource.org/licenses/MIT
6 */
7
8using NanoXLSX.Interfaces;
9using NanoXLSX.Interfaces.Reader;
10using NanoXLSX.Registry;
11using NanoXLSX.Registry.Attributes;
12using NanoXLSX.Utils;
13using NanoXLSX.Utils.Xml;
14using System;
15using System.Collections.Generic;
16using System.IO;
17using System.Xml;
18using IOException = NanoXLSX.Exceptions.IOException;
19
21{
25 [NanoXlsxQueuePlugIn(PlugInUUID = "EXTERNAL_LINK_READER", QueueUUID = PlugInUUID.ReaderPrependingQueue, PlugInOrder = 20000)]
26 internal class ExternalLinkReader : IDiscoveryPackageReader
27 {
28 #region privateFields
29
30 private Stream stream;
31
32 private const string ExternalLinkPathRelationshipType = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/externalLinkPath";
33 private const string OfficeRelationshipNamespace = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
34 private const string AlternateUrlNamespace = "http://schemas.microsoft.com/office/spreadsheetml/2021/extlinks2021";
35
36 #endregion
37
38 #region properties
42 public IOptions Options { get; set; }
46 public Workbook Workbook { get; set; }
50 public Action<Stream, Workbook, string, IOptions, int?> InlinePluginHandler { get; set; }
51
55 public string StreamEntryName => null; // NoOp
56
60 public RelationshipInfo CurrentRelationship { get; set; }
61
65 public string DocumentType { get { return "http://schemas.openxmlformats.org/officeDocument/2006/relationships/externalLink"; } }
66 #endregion
67
68 #region constructors
72 public ExternalLinkReader()
73 {
74 }
75 #endregion
76
77 #region methods
85 public void Init(Stream stream, Workbook workbook, IOptions readerOptions, Action<Stream, Workbook, string, IOptions, int?> inlinePluginHandler)
86 {
87 this.stream = stream;
88 this.Workbook = workbook;
89 this.Options = readerOptions;
90 this.InlinePluginHandler = inlinePluginHandler;
91 }
92
97 public void Execute()
98 {
99 Dictionary<int, ExternalWorksheet> worksheets = new Dictionary<int, ExternalWorksheet>();
100 List<ExternalDefinedName> definedNames = new List<ExternalDefinedName>();
101 string targetRelationshipId = null;
102 string absoluteAlternateRelationshipId = null;
103 string relativeAlternateRelationshipId = null;
104 try
105 {
106 using (XmlReader reader = XmlReader.Create(stream, XmlStreamUtils.CreateSettings()))
107 {
108 bool isExternalBook = false;
109 while (reader.Read())
110 {
111 if (XmlStreamUtils.IsElement(reader, "externalBook"))
112 {
113 isExternalBook = true;
114 targetRelationshipId = reader.GetAttribute("id", OfficeRelationshipNamespace);
115 }
116
117 if (isExternalBook)
118 {
119 if (reader.NamespaceURI == AlternateUrlNamespace && XmlStreamUtils.IsElement(reader, "absoluteUrl"))
120 {
121 absoluteAlternateRelationshipId = reader.GetAttribute("id", OfficeRelationshipNamespace);
122 }
123 else if (reader.NamespaceURI == AlternateUrlNamespace && XmlStreamUtils.IsElement(reader, "relativeUrl"))
124 {
125 relativeAlternateRelationshipId = reader.GetAttribute("id", OfficeRelationshipNamespace);
126 }
127 else if (XmlStreamUtils.IsElement(reader, "sheetNames"))
128 {
129 GetSheeetNames(reader.ReadSubtree(), worksheets);
130 if (worksheets.Count == 0)
131 {
132 throw new IOException("No cached worksheets could be determined");
133 }
134 }
135 else if (XmlStreamUtils.IsElement(reader, "sheetDataSet"))
136 {
137 GetSheetData(reader.ReadSubtree(), worksheets);
138 }
139 else if (XmlStreamUtils.IsElement(reader, "definedNames"))
140 {
141 GetDefinedNames(reader.ReadSubtree(), definedNames);
142 }
143 }
144 }
145 }
146 ExternalLink link = new ExternalLink();
147 RelationshipCatalog discoveryCatalog = Workbook.AuxiliaryData.GetData<RelationshipCatalog>(PlugInUUID.DiscoveryReader, PlugInUUID.DiscoveryCatalogEntity);
148 if (discoveryCatalog == null)
149 {
150 throw new IOException("The relationship catalog is not available for the external link.");
151 }
152 string sourcePartPath = CurrentRelationship.ResolvedTargetPath;
153 RelationshipInfo target = GetExternalLinkPathRelationship(discoveryCatalog, sourcePartPath, targetRelationshipId, "target", true);
154 RelationshipInfo absoluteAlternate = GetExternalLinkPathRelationship(discoveryCatalog, sourcePartPath, absoluteAlternateRelationshipId, "absolute alternate", false);
155 RelationshipInfo relativeAlternate = GetExternalLinkPathRelationship(discoveryCatalog, sourcePartPath, relativeAlternateRelationshipId, "relative alternate", false);
156
157 link.SetReadUris(target.Target, absoluteAlternate?.Target, relativeAlternate?.Target);
158 foreach (KeyValuePair<int, ExternalWorksheet> worksheet in worksheets)
159 {
160 link.AddWorksheet(worksheet.Value);
161 }
162 foreach (ExternalDefinedName definedName in definedNames)
163 {
164 link.AddDefinedName(definedName);
165 }
166 link.WorkbookRId = CurrentRelationship.Id;
167
168 List<ExternalLink> externalLinks = Workbook.AuxiliaryData.GetDataList<ExternalLink>(PlugInUUID.CompatibilityInlineProcessor, CompatibilityConstants.EXTERNAL_LINK_OBJECT_ENTITY);
169 int index = externalLinks.Count;
170 Workbook.AuxiliaryData.SetData(PlugInUUID.CompatibilityInlineProcessor, CompatibilityConstants.EXTERNAL_LINK_OBJECT_ENTITY, index, link, true);
171 }
172 catch (Exception ex)
173 {
174 throw new IOException("The XML entry could not be read from the " + nameof(stream) + ". Please see the inner exception:", ex);
175 }
176 }
177
188 private static RelationshipInfo GetExternalLinkPathRelationship(
189 RelationshipCatalog catalog,
190 string sourcePartPath,
191 string relationshipId,
192 string role,
193 bool required)
194 {
195 if (string.IsNullOrEmpty(relationshipId))
196 {
197 if (required)
198 {
199 throw new IOException("The external-link " + role + " relationship ID is missing.");
200 }
201 return null;
202 }
203
204 RelationshipInfo relationship = catalog.GetBySourceAndId(sourcePartPath, relationshipId);
205 if (relationship == null)
206 {
207 throw new IOException("The external-link " + role + " relationship '" + relationshipId + "' could not be resolved.");
208 }
209 if (!string.Equals(relationship.Type, ExternalLinkPathRelationshipType, StringComparison.Ordinal)
210 || relationship.TargetMode != System.IO.Packaging.TargetMode.External)
211 {
212 throw new IOException("The external-link " + role + " relationship '" + relationshipId + "' has an invalid type or target mode.");
213 }
214 return relationship;
215 }
216
222 private static void GetSheeetNames(XmlReader sheetNames, Dictionary<int, ExternalWorksheet> worksheets)
223 {
224 int index = 0;
225 while (sheetNames.Read())
226 {
227 if (XmlStreamUtils.IsElement(sheetNames, "sheetName"))
228 {
229 string val = sheetNames.GetAttribute("val");
230 ExternalWorksheet worksheet = new ExternalWorksheet(val);
231 worksheets[index] = worksheet;
232 index++;
233 }
234 }
235 }
236
243 private static void GetSheetData(XmlReader sheetDataSet, Dictionary<int, ExternalWorksheet> worksheets)
244 {
245 int currentIndex = -1;
246 ExternalWorksheet currentWorksheet = null;
247 while (sheetDataSet.Read())
248 {
249 if (XmlStreamUtils.IsElement(sheetDataSet, "sheetData"))
250 {
251 string id = sheetDataSet.GetAttribute("sheetId");
252 currentIndex = ParserUtils.ParseInt(id);
253 if (!worksheets.TryGetValue(currentIndex, out currentWorksheet))
254 {
255 throw new IOException("The cached worksheet with sheet ID '" + id + "' is not defined.");
256 }
257 string refreshErrors = sheetDataSet.GetAttribute("refreshErrors");
258 if (refreshErrors != null)
259 {
260 int parserdSate = ParserUtils.ParseBinaryBool(refreshErrors);
261 currentWorksheet.RefreshErros = parserdSate == 1 ? true : false;
262 }
263 continue;
264 }
265 else if (XmlStreamUtils.IsElement(sheetDataSet, "row"))
266 {
267 GetRowData(sheetDataSet.ReadSubtree(), currentWorksheet);
268 }
269 }
270 }
271
277 private static void GetDefinedNames(XmlReader definitions, List<ExternalDefinedName> definedNames)
278 {
279 while (definitions.Read())
280 {
281 if (!XmlStreamUtils.IsElement(definitions, "definedName"))
282 {
283 continue;
284 }
286 definitions.GetAttribute("name"),
287 definitions.GetAttribute("refersTo"),
288 false)
289 {
290 RelationshipId = definitions.GetAttribute("sheetId")
291 };
292 definedNames.Add(definedName);
293 }
294 }
295
301 private static void GetRowData(XmlReader row, ExternalWorksheet worksheet)
302 {
303 while (row.Read())
304 {
305 if (!XmlStreamUtils.IsElement(row, "cell"))
306 {
307 continue;
308 }
309
310 string address = row.GetAttribute("r");
311 string type = row.GetAttribute("t");
312 string cellMetaData = row.GetAttribute("vm"); // Roundtrip only
314 if (type != null)
315 {
316 switch (type)
317 {
318 case "b":
319 dataType = ExternalCellValue.DataType.Boolean;
320 break;
321 case "d":
322 dataType = ExternalCellValue.DataType.Date;
323 break;
324 case "e":
325 dataType = ExternalCellValue.DataType.Error;
326 break;
327 case "s": // Should not be used
328 case "str":
329 dataType = ExternalCellValue.DataType.String;
330 break;
331 }
332 }
333
334 string value = null;
335 if (!row.IsEmptyElement)
336 {
337 using (XmlReader cell = row.ReadSubtree())
338 {
339 while (cell.Read())
340 {
341 if (XmlStreamUtils.IsElement(cell, "v"))
342 {
343 value = cell.ReadElementContentAsString();
344 break;
345 }
346 }
347 }
348 }
349 worksheet.AddCell(address, value, dataType, cellMetaData);
350 }
351 }
352 #endregion
353 }
354
355}
Represents a cached cell of an external worksheet with an optional type.
DataType
Enum for the data type of the external cell.
Represents a defined name declared by an external workbook.
Represents a worksheet declared by an external workbook link.
ExternalWorksheet AddCell(string address, string value)
Adds or replaces a cached cell value.