A HotXLS Delphi Excel Component a v2.382.0 óta megőrzi az OpenDocument data pilot táblákat egy ODS megnyitás-mentés cikluson át úgy, hogy megnyitáskor szó szerint elkapja a content.xml <table:data-pilot-tables> részfáját, és mentéskor visszajátssza. A v2.382.1 óta a fragmentum az összes olyan XML-névtérkötést is hordozza, amelyet az ősei deklaráltak, így a mentett pivotdefiníció minden feldolgozó számára jól formált marad, nem csak a HotXLS számára
A hibát, amely mindkét változtatást kikényszerítette, egy szigorú korpuszfutás hozta elő. Az official-pivot.ods minta, amelyet egy LibreOffice 6.1 fejlesztői build írt, egyetlen DataPilot1 nevű pivotot tartalmaz, amely a Sheet1.A2:E30 tartományt olvassa, és az eredményét a Sheet1.G6:J18 helyre teszi. Nyissa meg HotXLS-szel, mentse el változtatás nélkül, és számolja meg a <table:data-pilot-table> elemeket a kimenetben: egy be, nulla ki, Win32 és Win64 alatt egyaránt. A tesztben semmi nem nyúlt a pivothoz. Az első ellenőrzési kör csak a cellakonstansokat hasonlította össze, és átment; a szerkezeti állítás volt az, amely leleplezte a veszteséget, ami emlékeztető arra, hogy az „az értékek egyeznek" gyenge definíciója a körbejáratási hűségnek
Miért tűnik el egy ODS pivot tábla a könyvtárbeli mentés után?
Az ODS pivot tábla azért tűnik el, mert a HotXLS-nek nincs memóriabeli modellje az OpenDocument data pilot táblákhoz, az ODS-író pedig a content.xml fájlt teljes egészében a modellből építi fel. Az író összeállítja az automatikus stílusokat, munkalaponként egy <table:table> elemet, a <table:content-validations>, <table:named-expressions> és <table:database-ranges> blokkokat, mindegyiket a munkafüzetben ténylegesen meglévő objektumokból generálva. Egy pivotdefiníciónak — ODF 1.3 Part 3 §9.6, egy <table:data-pilot-tables> konténer pivotonként egy <table:data-pilot-table> elemmel, amely hordozza a table:source-cell-range elemét, a table:data-pilot-field gyerekeit, valamint a table:target-range-address és a table:buttons elemeit — nincs objektuma, amelyben élhetne, így az újragenerált rész egyszerűen kihagyja
Az XLSX-hez képesti ellentét szándékos. A HotXLS a SpreadsheetML pivot cache-eket és pivot táblákat valódi modellé dolgozza fel, amelyet Delphiből felépíthet, számított mezőkkel bővíthet és frissíthet, így azok túlélik a mentést, mert újraíródnak, nem másolódnak. Az ODS pivotok sokkal ritkább kérés, az ODF data pilot szókészlet modellezése pedig pusztán a körbejáratás kedvéért rengeteg olyan kód lenne, amelyhez senki nem nyúl. A pragmatikus válasz ugyanaz, amelyet a HotXLS már alkalmaz az XLSX ismeretlen extLst blokkjaira: őrizze meg, amit nem modellez, bájtról bájtra, ha lehet, eseményről eseményre, ha nem
Mit hibázott el az első, Pos-alapú kinyerés?
A v2.382.0-ban a kinyerés sima sztringként vágta ki a pivotdefiníciót a content.xml-ből, a kivágott darabból pedig hiányoztak azok a névtér-deklarációk, amelyek értelmessé tették. Az implementáció pont olyan rövid, amilyennek hangzik — dekódolja a részt egy WideString-be, megkeresi a nyitó taget a Pos hívással, megkeresi utána a záró taget, és a kifogott szakaszt a munkafüzet FRawOdsDataPilotTablesXml mezőjébe másolja:
// HotXLS v2.382.0 -- egy kiadással később felváltva
function OdsCaptureDataPilotTablesXml(Stream: TStream): WideString;
const
OpenTag: WideString = '<table:data-pilot-tables';
CloseTag: WideString = '</table:data-pilot-tables>';
var
Text: WideString;
StartPos, ClosePos: Integer;
begin
Result := '';
Text := LoadPartAsWideString(Stream); // a teljes content.xml a memóriában
StartPos := Pos(OpenTag, Text);
if StartPos = 0 then Exit;
ClosePos := Pos(CloseTag, Copy(Text, StartPos, MaxInt));
if ClosePos = 0 then Exit;
Result := Copy(Text, StartPos, ClosePos + Length(CloseTag) - 1);
end;
A darabszám-ellenőrzés zöldre váltott, a javítás pedig megjelent. Az fogta el, hogy még aznap bekerült egy második, szigorúbb ellenőrzés: a mentett csomag minden XML-részét átadjuk egy független, névtér-tudatos elemzőnek a HotXLS-en kívül, és az az elemző az új content.xml-t egy nem kötött előtag hibájával utasította el. A LibreOffice-ból származó pivot gyártóspecifikus kiterjesztés-attribútumokat hordoz — loext:ignore-selected-page="true" egy oldalmezőn, calcext:repeat-item-labels="false" minden szinten —, a kivágott sztring pedig tartalmazta ezeket az attribútumokat, de nem tartalmazta az őket kötő xmlns:loext és xmlns:calcext deklarációkat. Azok a deklarációk a forrásfájl <office:document-content> gyökerén ültek, harmincöt darab, kétezer karakterre a pivottól
A W3C Namespaces in XML 1.0 §6.1 írja le azt a szabályt, amely ezt kemény hibává, nem pedig kozmetikaivá teszi: egy névtér-deklaráció annak az elemnek a nyitó tagjától érvényes, amelyen szerepel, egészen ugyanazon elem záró tagjáig, és az ezen a hatókörön belüli minden előtaggal írt név ehhez oldódik fel. Vágjon ki egy részfát a dokumentumból, és ki fogja vágni a hatókörből is. A HotXLS a saját <office:document-content> gyökerét tizenegy deklarációval írja — office, table, text, style, number, fo, draw, svg, xlink, calcext, tableooo —, így a calcext: épp feloldódott, a table: épp feloldódott, a loext: viszont nem. A névtér-tudatos elemző a nem kötött előtagot jólformáltsági hibaként kezeli, ami azt jelenti, hogy az egész rész olvashatatlan, nem csupán egyetlen attribútum
Hogyan viszi rá a HotXLS az ős xmlns-kötéseket a fragmentumra?
A HotXLS v2.382.1 lecserélte a sztringkivágást egy, a saját streamelő TXMLReader osztályán keresztül vezető menetre a content.xml felett: veremben tartja a névtérkötéseket, megjelölve mindegyik deklarálásának mélységét, és abban a pillanatban, amikor eléri a cél elemet, rámásolja a fragmentum gyökér elemére a még érvényben lévő kötéseket. Az olvasó bekapcsolt PreserveWhitespaceText mellett fut, így a szöveges csomópontok pontosan úgy jönnek vissza, ahogy meg vannak írva, az újraépített tagek pedig a TXMLReader.RawName és a TXMLReader.Attribute[I].RawName értékét használják — az előtag fájlbeli írásmódját —, nem azokat a kanonikus neveket, amelyeket az olvasó egyébként a részelemzőknek ad. Íme a ciklus magja:
// Namespaces: TStringList 'xmlns:p=uri' alakú sorokból, a deklarálási mélység az Objects[] tömbben
while Reader.Read do
begin
if CaptureDepth >= 0 then
XlsxAppendRawXmlReaderNode(Result, Reader); // elem, szöveg, CDATA, megjegyzés
if Reader.NodeType = xmlntElement then
begin
for I := 0 to Reader.AttributeCount - 1 do
begin
AttrName := Reader.Attribute[I].RawName;
if (AttrName = 'xmlns') or (Pos(WideString('xmlns:'), AttrName) = 1) then
Namespaces.AddObject(String(AttrName) + '=' + String(Reader.Attribute[I].Value),
TObject(NativeInt(Depth)));
end;
if (CaptureDepth < 0) and (Reader.Name = 'table:data-pilot-tables') then
begin
Opening := XlsxRawXmlReaderOpenTag(Reader); // előbb lehántjuk a záró '>' vagy '/>' jelet
...
// A hatályos ős-kötések rámásolása a fragmentum gyökerére.
for I := Namespaces.Count - 1 downto 0 do
begin
AttrName := WideString(Namespaces.Names[I]);
if Seen.IndexOf(String(AttrName)) >= 0 then Continue; // a legbelső kötés nyer
Seen.Add(String(AttrName));
if not Reader.HasAttribute(AttrName) then // itt már deklarálva van? kihagyjuk
Opening := Opening + ' ' + AttrName + '="' +
XlsxEscapeAttr(WideString(Namespaces.ValueFromIndex[I])) + '"';
end;
...
CaptureDepth := Depth;
end;
if not Reader.IsEmptyElement then Inc(Depth);
end
else if Reader.NodeType = xmlntEndElement then
begin
Dec(Depth);
if Depth = CaptureDepth then Exit; // a részfa lezárult
end;
if (Reader.NodeType = xmlntEndElement) or
((Reader.NodeType = xmlntElement) and Reader.IsEmptyElement) then
while (Namespaces.Count > 0) and
(NativeInt(Namespaces.Objects[Namespaces.Count - 1]) >= Depth) do
Namespaces.Delete(Namespaces.Count - 1); // kilépünk a hatókörből
end;
if CaptureDepth >= 0 then
raise Exception.Create('OpenDocument pivot definition ended inside an element');
Három részlet hordozza ebben a ciklusban a helyességet. A verem bejárása a legbelső kötéstől kifelé, és az egyes előtagok megjegyzése a Seen halmazban valósítja meg az árnyékolást: ha egy közelebbi ős újraköti az xmlns:table-t, a közelebbi érték nyer, pontosan ahogy a §6.1 megköveteli. Azoknak az előtagoknak a kihagyása, amelyeket az elem maga már deklarál, megakadályozza ugyanannak az attribútumnak a kétszeri kibocsátását, ami egy másfajta jólformáltsági hiba volna. A levesz-szabály pedig a záró tageknél és az üres elemeknél is tüzel, mert a <x/> soha nem hoz létre EndElement eseményt — ugyanaz az önmagát lezáró csapda, amelyet az XLSX extLst kinyerésének is meg kellett tanulnia. A cél megcélzása a Reader.Name alapján a RawName helyett csendesebb győzelem: az olvasó az ODF table névtér URI-ját a table előtagra kanonizálja, így az a gyártó is illeszkedik, amely t:data-pilot-tables alakban írja, a kibocsátott fragmentum pedig megtartja, milyen előtagot használt a gyártó
A ciklus a tippelést is visszautasítja. Ha a rész véget ér, miközben a kinyerés még nyitva van — csonkolt vagy hibás content.xml —, az OdsCaptureDataPilotTablesXml kivételt dob a fél fragmentum visszaadása helyett, mert a fél fragmentum mentéskor visszaíródna, és egy sérült bemenetből a könyvtár nevével ellátott sérült kimenetet csinálna
Hová kerül a fragmentum a mentett content.xml-ben?
A HotXLS a kinyert fragmentumot az <office:spreadsheet> elembe írja, közvetlenül az általa generált <table:named-expressions> után és a <table:database-ranges> előtt. Az <office:spreadsheet> ODF 1.3 Part 3 szerinti tartalmi modellje rögzített sorrendet ír elő ezekre a záró gyerekelemekre, így egy szó szerinti blokkot nem lehet egyszerűen oda fűzni, ahol az író épp tart; egy meghatározott helyre kell betenni. A hívó oldaláról nincs API és nincs mit konfigurálni; a definíció egy szokásos megnyitással és mentéssel együtt utazik:
var
Book: TXLSXWorkbook;
begin
Book := TXLSXWorkbook.Create;
try
if Book.OpenODS('official-pivot.ods') <> 1 then
raise Exception.Create('open failed');
Book.Sheets[0].Cells[2, 5].Value := 1250.0; // szerkesztés a pivot forrástartományán belül
Book.SaveAsODS('official-pivot-out.ods');
// a kimenet content.xml fájlja továbbra is hordozza a DataPilot1-et a
// forrástartományával, mezőivel, céltartományával, gombjaival és loext:/calcext: attribútumaival
finally
Book.Free;
end;
end;
A redundancia szándékos, és érdemes tudni róla. A fragmentum gyökere mostantól megismétli az xmlns:table és xmlns:calcext deklarációt, holott a mentett dokumentum gyökere is deklarálja őket; a Namespaces in XML megengedi egy előtag újradeklarálását beágyazott hatókörben, így a duplikátumok ártalmatlanok. A LibreOffice minta esetében az átvitt halmaz mind a harmincöt gyökérdeklaráció, mintegy két kilobájt a 8,357 karakteres definíció tetején, mert a kinyerés nem elemzi, hogy a részfa valójában mely előtagokat használja. Egy használt-előtag vizsgálat lefaragná ezt, és talán később meg is érkezik; előbb a helyesség, aztán a tömörség
Szabály arra, hogyan vágjunk részfákat XML-ből szó szerinti visszajátszáshoz
Az általános tanulság az, hogy egy részfa csak akkor önmagában megálló, ha önmagában megállóvá tettük, és a névtérhatókör az első dolog, ami eltörik, ha erről megfeledkezünk. Az ellenőrzőlista, amelyet a HotXLS mostantól minden olyan kinyerésre alkalmaz, amelynek a lényege: őrizze meg, amit nem modellez:
- Járja be a dokumentumot valódi olvasóval, és kövesse nyomon a hatókörben lévő kötéseket. A
Pos-szal végzett sztringkeresés egyáltalán nem látja a hatókört, ráadásul tévesen illeszkedik azonos nevű beágyazott elemekre, megjegyzésben vagyCDATAszakaszban lévő egyező sztringre, valamint olyan attribútumértékekre, amelyek épp tartalmazzák a tag szövegét - Másolja a hatályos kötéseket a fragmentum gyökerére, belülről kifelé, előtagoként egyszer, kihagyva azt, amit a gyökér maga már deklarál
- Tartsa meg az előtagok nyers írásmódját a kibocsátott tagekben; a célpontot a feloldott névtér alapján illessze, ne a szó szerinti előtag alapján
- Őrizze meg a whitespace szöveges csomópontokat, és ne feledje, hogy egy üres elem zárótag-esemény nélkül zárja le a saját hatókörét
- Érvényesítse a mentett részt olyan elemzővel, amely nem a vizsgált könyvtár. A könyvtár boldogan visszaolvassa a saját kimenetét ugyanazon az elnéző kódúton, amely megírta
Az utolsó pont az, amely valójában másodszor is megtalálta a HXLS-003-at. A v2.382.0 elfogadási ellenőrzése egy reguláris kifejezés volt, amely a mentett content.xml-ben megszámolta a data-pilot-table nyitó tageket, a reguláris kifejezés pedig taget lát, nem dokumentumot — vak arra, hogy a tagen lévő előtagok kötöttek-e. A v2.382.1-ben hozzáadott szigorú korpuszfuttató a mentett csomag minden XML- és .rels részét névtér-tudatos elemzővel dolgozza fel, majd a pivotfát — tag, rendezett attribútumok, szöveg, gyerekek, rekurzívan — összeveti az eredetivel. Ez az összehasonlítás névtérrel kiterjesztett, így az előtag más írásmódja is átmenne, a kötetlen előtag viszont nem mehet át
Hol ér véget a szó szerinti garancia
A szó szerinti visszajátszás megőriz egy definíciót; nem érti meg, és a határok ebből következnek. A HotXLS nem tesz közzé API-t ODS pivot olvasására, szerkesztésére vagy frissítésére, így a FRawOdsDataPilotTablesXml belső mező, az egyetlen megfigyelhető viselkedés pedig az, hogy a definíció túléli az utat. A fragmentum olvasói eseményekből szerializálódik újra, nem bájtokként másolódik: az attribútumok idézőjelezése és az önmagukat lezáró formák normalizálódnak, a szöveg és a whitespace viszont megmarad. A kinyert XML-t kizárólag az ODS-tartalomíró bocsátja ki, így egy .ods-ből megnyitott és .xlsx-ként mentett munkafüzet elveszíti a pivotot, egy .xlsx-ből megnyitott munkafüzetnek pedig nincs mit visszajátszania egy .ods-mentésbe — az ODS import- és exportutak aszimmetriái itt is érvényesek, mint mindenhol. És mivel a definíció átlátszatlan, nem tudja követni a szerkesztéseit: nevezze át a Sheet1-et, vagy mozgassa el a forrásadatokat a HotXLS-ben, és a mentett pivot továbbra is a Sheet1.A2:E30 helyre mutat, a feldolgozónak pedig a következő frissítésnél törött tartományt kell jelentenie. Ide tartozik egy sorrendi figyelmeztetés is: a HotXLS az AutoFilter-tartományokat <table:database-ranges> formában a pivot-fragmentum után bocsátja ki, a korpuszminta pedig nem hordoz adatbázistartományt, így azt a munkafüzetet, amelyben szűrő és pivot is van, érdemes átfuttatni egy ODF-sémaérvényesítőn, mielőtt a két elem egymáshoz viszonyított sorrendjére támaszkodna
Teszteljen a saját gyártója fájljaival, ne csak a korpuszmintával. A névtér-átvitel minden olyan előtagot kezel, amelyet a gyártó egy ősön deklarál, de az a dokumentum, amely magán a pivot elemen deklarál előtagot, vagy amely alapértelmezett névteret használ a table szókészlethez, azokat a kihagyási és árnyékolási ágakat gyakorolja, amelyeket a LibreOffice minta nem. Mindkettő meg van valósítva; egyikhez sincs még minta a korpuszban, és pontosan ez az a fajta különbség, amelyet egy changelog-bejegyzés hajlamos elmosni
A v2.382.0 szó szerinti data pilot kinyerése és a v2.382.1 névtérhatókört javító módosítása a jelenlegi HotXLS Delphi Excel Componentben érkezik, amelynek termékoldala felsorolja a teljes ODS-, XLSX- és XLS-olvasási-írási lefedettséget Delphihez és C++Builderhez