Teknik Makale

Delphi'de ODS Pivot Table Round-Trip: XML Namespace Kapsamı

HotXLS Delphi Excel Component, OpenDocument data pilot tablolarını bir ODS aç-kaydet döngüsü boyunca koruyor: açma anında content.xml dosyasının <table:data-pilot-tables> alt ağacını birebir yakalıyor ve kayıtta yeniden oynatıyor; bu davranış v2.382.0'dan beri böyle. v2.382.1'den beri fragman, üst öğelerinin bildirdiği her XML namespace bağını da taşıyor, böylece kaydedilen pivot tanımı yalnızca HotXLS için değil her tüketici için well-formed kalıyor

Her iki değişikliği zorunlu kılan hata, sıkı bir corpus koşusundan çıktı. Bir LibreOffice 6.1 geliştirme derlemesiyle yazılmış official-pivot.ods örneği, Sheet1.A2:E30 okuyup sonucunu Sheet1.G6:J18 alanına yazan DataPilot1 adlı tek bir pivot içeriyor. HotXLS ile açın, değiştirmeden kaydedin, çıktıdaki <table:data-pilot-table> öğelerini sayın: giren bir, çıkan sıfır; hem Win32 hem Win64 için böyle. Testte hiçbir şey pivot'a dokunmuyordu. İlk sorgu turu yalnızca hücre sabitlerini karşılaştırmış ve geçmişti; kaybı ortaya çıkaran yapısal assertion oldu — bu da değerler uyuşuyor ifadesinin round-trip sadakati için zayıf bir tanım olduğunu hatırlatıyor

Bir ODS pivot tablosu kütüphane kaydından sonra neden kayboluyor?

Bir ODS pivot tablosu kayboluyor, çünkü HotXLS'in OpenDocument data pilot tabloları için bellekte bir modeli yok ve ODS yazıcısı content.xml dosyasını tamamen modelden kuruyor. Yazıcı otomatik stilleri, her worksheet için bir <table:table>, <table:content-validations>, <table:named-expressions> ve <table:database-ranges> öğelerini, her biri workbook'un gerçekten taşıdığı nesnelerden üretilecek biçimde bir araya getiriyor. Bir pivot tanımının — ODF 1.3 Part 3 §9.6, pivot başına bir <table:data-pilot-table> içeren <table:data-pilot-tables> kabı; beraberinde table:source-cell-range, table:data-pilot-field çocukları, table:target-range-address ve table:buttons — yaşayacağı bir nesne yok, bu yüzden yeniden üretilen parça onu basitçe atlıyor

XLSX ile aradaki karşıtlık bilinçli. HotXLS, SpreadsheetML pivot cache'lerini ve pivot tablolarını Delphi'den kurup hesaplanmış alanlarla genişletebileceğiniz ve yenileyebileceğiniz gerçek bir modele ayrıştırır; bunlar kayıttan sağ çıkar çünkü kopyalanmaz, yeniden yazılırlar. ODS pivot'ları çok daha seyrek gelen bir istek ve yalnızca round-trip uğruna ODF data pilot sözlüğünü modellemek, kimsenin düzenlemediği bir sürü kod demek olurdu. Pragmatik yanıt, HotXLS'in XLSX'teki bilinmeyen extLst bloklarına zaten uyguladığı yanıtın aynısı: modellemediğini koru; yapabiliyorsan bayt bayt, yapamıyorsan olay olay

İlk Pos tabanlı yakalama neyi yanlış yaptı?

v2.382.0'daki yakalama, pivot tanımını content.xml içinden düz bir string olarak kesip çıkarıyordu ve kesilen parça, tanımı anlamlı kılan namespace bildirimlerini içermiyordu. Uygulama, kulağa geldiği kadar kısaydı: parçayı bir WideString olarak çöz, açılış etiketini Pos ile bul, kapanış etiketini ondan sonra ara, aralığı workbook üzerindeki FRawOdsDataPilotTablesXml alanına kopyala:

// HotXLS v2.382.0 -- bir sürüm sonra geçersiz kaldı
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);   // content.xml'in tamamı bellekte
  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;

Sayım assertion'ı yeşile döndü ve düzeltme yayınlandı. Onu yakalayan şey aynı gün eklenen ikinci ve daha sıkı bir kontrol oldu: kaydedilen paketin her XML parçası HotXLS dışındaki bağımsız, namespace farkında bir parser'a veriliyor ve o parser yeni content.xml dosyasını bağlanmamış prefix hatasıyla reddetti. LibreOffice'ten gelen pivot, üretici uzantı öznitelikleri taşıyor — bir sayfa alanında loext:ignore-selected-page="true", her düzeyde calcext:repeat-item-labels="false" — ve kesilen string bu öznitelikleri içeriyordu ama onları bağlayan xmlns:loext ile xmlns:calcext bildirimlerini içermiyordu. O bildirimler kaynak dosyanın <office:document-content> kökünde duruyordu; otuz beş tanesi, pivot'tan iki bin karakter uzakta

W3C Namespaces in XML 1.0 §6.1, bunu kozmetik değil katı bir hata yapan kuralı tanımlar: bir namespace bildirimi, bulunduğu öğenin başlangıç etiketinden o öğenin bitiş etiketine kadar kapsam içindedir ve o kapsam içindeki her prefix'li ad ona göre çözümlenir. Bir alt ağacı belgeden kesip çıkarırsanız onu kapsamın dışına da çıkarırsınız. HotXLS kendi <office:document-content> kökünü on bir bildirimle yazıyor — office, table, text, style, number, fo, draw, svg, xlink, calcext, tableooo — bu yüzden calcext: şans eseri çözümlendi, table: şans eseri çözümlendi, loext: ise çözümlenemedi. Namespace farkında bir parser, bağlanmamış bir prefix'i well-formedness ihlali sayar; bu da tek bir özniteliğin değil, parçanın tamamının okunamaz olduğu anlamına gelir

HotXLS'te official-pivot.ods dosyasının Pos tabanlı yakalamasının kaçırdığı şey: pivot alt ağacı loext ve calcext uzantı öznitelikleri taşırken onları bağlayan xmlns bildirimleri otuz beş bağ uzakta office:document-content kökünde duruyor, bu yüzden kesilen fragman kullandığı her prefix'i bağsız bıraktı ve namespace farkında bir parser content.xml'in tamamını reddetti
Bir namespace bildirimi başlangıç etiketinden bitiş etiketine kadar kapsam içindedir ve bir alt ağacı belgeden kesmek onu o kapsamın dışına çıkarır, bu da tek bir özniteliği okunamaz bir parçaya dönüştürür

HotXLS üst öğelerin xmlns bağlarını fragmana nasıl taşıyor?

HotXLS v2.382.1, string kesme işleminin yerine content.xml üzerinde kendi streaming TXMLReader'ıyla bir geçiş koydu; her bağın bildirildiği derinlikle etiketlenmiş bir namespace bağı yığını tutuyor ve hedefe ulaşıldığı anda hâlâ yürürlükte olan bağları fragmanın kök öğesine kopyalıyor. Reader, PreserveWhitespaceText etkin olarak çalışıyor, böylece metin düğümleri yazıldığı gibi geri geliyor; yeniden kurulan etiketler ise reader'ın parça ayrıştırıcılarına normalde verdiği kanonik adlar yerine TXMLReader.RawName ve TXMLReader.Attribute[I].RawName değerlerini, yani dosyadaki prefix yazımını kullanıyor. Döngünün çekirdeği şöyle:

HotXLS v2.382.1'in data pilot alt ağacını namespace kapsamıyla birlikte nasıl yakaladığı: streaming TXMLReader geçişi, bildiren derinlikle etiketlenmiş xmlns bağlarından oluşan bir yığın tutar, table:data-pilot-tables hedefinde yığını en içten dışa doğru yürür, gölgelemeyi Seen kümesiyle uygular, öğenin kendi bildirdiği prefix'leri atlar ve bağları hem bitiş etiketlerinde hem boş öğelerde yığından çıkarır
Hedefi kanonik reader adıyla eşleştirmek, table prefix'ini başka türlü yazan üreticileri de çalıştırır; hiç kapanmayan bir alt ağaç ise kayıtta yarım fragman yazmak yerine istisna fırlatır
// Namespaces: 'xmlns:p=uri' girdilerini tutan TStringList; bildiren derinlik Objects[] içinde
while Reader.Read do
begin
  if CaptureDepth >= 0 then
    XlsxAppendRawXmlReaderNode(Result, Reader);   // öğe, metin, CDATA, yorum
  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);   // önce sondaki '>' ya da '/>' kırpılır
      ...
      // Yürürlükteki üst öğe bağlarını fragman köküne taşı.
      for I := Namespaces.Count - 1 downto 0 do
      begin
        AttrName := WideString(Namespaces.Names[I]);
        if Seen.IndexOf(String(AttrName)) >= 0 then Continue;   // en içteki bağ kazanır
        Seen.Add(String(AttrName));
        if not Reader.HasAttribute(AttrName) then               // burada zaten bildirilmiş mi? atla
          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;                           // alt ağaç kapandı
  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);                   // kapsamdan çık
end;
if CaptureDepth >= 0 then
  raise Exception.Create('OpenDocument pivot definition ended inside an element');

Bu döngüde doğruluğu üç ayrıntı taşıyor. Yığını en içteki bağdan dışa doğru yürümek ve her prefix'i Seen içinde hatırlamak gölgelemeyi uygular: daha yakın bir üst öğe xmlns:table bağını yeniden bildirirse yakın olan değer kazanır, tam da §6.1'in gerektirdiği gibi. Öğenin kendi bildirdiği prefix'leri atlamak aynı özniteliğin iki kez yazılmasını önler; bu da başka bir well-formedness hatası olurdu. Çıkarma kuralı ise bitiş etiketlerinde ve boş öğelerde tetiklenir, çünkü <x/> hiçbir zaman bir EndElement olayı üretmez — XLSX extLst yakalamasının da öğrenmek zorunda kaldığı aynı self-closing tuzağı. Hedefi RawName yerine Reader.Name ile eşleştirmek daha sessiz bir kazanç: reader, ODF table namespace URI'sini table prefix'ine kanonikleştiriyor, böylece onu t:data-pilot-tables diye yazan bir üretici de eşleşiyor; yazılan fragman ise üreticinin kullandığı prefix'i koruyor

Döngü ayrıca tahmin yürütmeyi reddediyor. Yakalama hâlâ açıkken parça biterse — kesilmiş ya da bozuk bir content.xml — OdsCaptureDataPilotTablesXml yarım bir fragman döndürmek yerine istisna fırlatıyor; çünkü yarım fragman kayıtta geri yazılır ve hasarlı bir girdiyi, üzerinde kütüphanenin adı olan hasarlı bir çıktıya dönüştürürdü

Fragman kaydedilen content.xml içinde nereye yerleşiyor?

HotXLS, yakalanan fragmanı <office:spreadsheet> içine, ürettiği <table:named-expressions> öğesinden hemen sonra ve <table:database-ranges> öğesinden önce yazıyor. <office:spreadsheet> öğesinin ODF 1.3 Part 3 içerik modeli bu sondaki çocuklar için sabit bir sıra öngörür, yani birebir bir blok yazıcının o an bulunduğu yere eklenemez; belirli bir yuvaya bırakılması gerekir. Çağıran tarafında hiçbir API ve yapılandırılacak hiçbir şey yok; tanım sıradan bir aç-kaydet ile birlikte geliyor:

Yakalanan pivot tanımının bir HotXLS ODS kaydında nereye düştüğü: office:spreadsheet çocukları üretilen table öğelerinden table:content-validations ve table:named-expressions boyunca sabit ODF sırasını izler, birebir table:data-pilot-tables fragmanı table:database-ranges öğesinden önceki yuvaya yerleşir ve hiçbir API yoktur çünkü tanım OpenODS ile SaveAsODS çağrılarıyla birlikte taşınır
Birebir bir blok, yazıcının o an bulunduğu yere eklenemez; taşıdığı üst öğe bağlarının kopyaları ise zararsızdır çünkü Namespaces in XML iç içe kapsamda bir prefix'in yeniden bildirilmesine izin verir
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;   // pivot kaynak aralığının içinde düzenleme
    Book.SaveAsODS('official-pivot-out.ods');
    // çıktıdaki content.xml hâlâ DataPilot1'i şunlarla birlikte taşıyor:
    // kaynak aralığı, alanlar, hedef aralık, buttons ve loext:/calcext: öznitelikleri
  finally
    Book.Free;
  end;
end;

Bu fazlalık bilinçli ve bilinmeye değer. Fragman kökü artık xmlns:table ve xmlns:calcext bağlarını, kaydedilen belge kökü de onları bildirse bile tekrarlıyor; Namespaces in XML bir prefix'in iç içe kapsamda yeniden bildirilmesine izin verir, bu yüzden tekrarlar zararsız. LibreOffice örneği için taşınan küme otuz beş kök bildirimin tamamı, yani 8.357 karakterlik tanımın üzerine yaklaşık iki kilobayt; çünkü yakalama, alt ağacın hangi prefix'leri gerçekten kullandığını analiz etmiyor. Kullanılan prefix'leri taramak bunu kırpardı ve belki ileride gelir; önce doğruluk, sonra kompaktlık

Birebir replay için XML'den alt ağaç kesme kuralı

Genel ders şu: bir alt ağaç ancak siz onu kendi kendine yeterli hâle getirdikten sonra kendi kendine yeterlidir ve unuttuğunuzda ilk kırılan şey namespace kapsamıdır. HotXLS'in artık modellemediğini koru türünden her yakalamaya uyguladığı kontrol listesi:

  • Belgeyi gerçek bir reader ile dolaşın ve kapsamdaki bağları takip edin. Pos ile string arama kapsamı hiç göremez; ayrıca aynı adlı iç içe öğelerde, bir yorum ya da CDATA bölümü içindeki eşleşen string'de ve etiket metnini içeren öznitelik değerlerinde de yanlış eşleşir
  • Yürürlükteki bağları fragman köküne kopyalayın; en içtekinden başlayarak, prefix başına bir kez ve kökün zaten bildirdiklerini atlayarak
  • Yazılan etiketlerde ham prefix yazımını koruyun; hedefi harfi harfine prefix ile değil, çözümlenmiş namespace ile eşleştirin
  • Whitespace metin düğümlerini koruyun ve boş bir öğenin kendi kapsamını bitiş etiketi olayı olmadan kapattığını unutmayın
  • Kaydedilen parçayı, test edilen kütüphanenin kendisi olmayan bir parser ile doğrulayın. Kütüphane kendi çıktısını, onu yazan aynı hoşgörülü kod yolundan memnuniyetle yeniden okur

Asıl önemli olan son madde; HXLS-003'ü ikinci kez bulan da o oldu. v2.382.0 kabul kontrolü, kaydedilen content.xml içindeki data-pilot-table başlangıç etiketlerini sayan bir düzenli ifadeydi ve düzenli ifade bir etiketi görür, bir belgeyi değil: o etiketteki prefix'lerin bağlı olup olmadığına kördür. v2.382.1'de eklenen sıkı corpus koşucusu, kaydedilen paketin her XML ve .rels parçasını namespace farkında bir parser ile ayrıştırıp pivot ağacını — etiket, sıralanmış öznitelikler, metin, çocuklar, özyinelemeli olarak — orijinalle karşılaştırır. Bu karşılaştırma namespace açılımlıdır, dolayısıyla prefix'in yeniden yazımı yine geçer, bağlanmamış bir prefix ise geçemez

Birebir garanti nerede bitiyor

Birebir replay bir tanımı korur; onu anlamaz ve sınırlar da bundan çıkar. HotXLS bir ODS pivot'unu okumak, düzenlemek ya da yenilemek için hiçbir API sunmuyor, bu yüzden FRawOdsDataPilotTablesXml dahili bir alan ve gözlemlenebilir tek davranış tanımın hayatta kalması. Fragman bayt olarak kopyalanmaz, reader olaylarından yeniden serileştirilir: öznitelik tırnaklaması ve self-closing biçimler normalleştirilir, metin ve whitespace ise korunur. Yakalanan XML yalnızca ODS içerik yazıcısı tarafından yazılır; yani .ods olarak açılıp .xlsx olarak kaydedilen bir workbook pivot'u kaybeder, .xlsx olarak açılan bir workbook'un ise bir .ods kaydına oynatacak hiçbir şeyi yoktur — ODS içe ve dışa aktarma yollarının asimetrileri burada da her yerde olduğu gibi geçerli. Tanım opak olduğu için düzenlemelerinizi de takip edemez: HotXLS içinde Sheet1 adını değiştirin ya da kaynak veriyi taşıyın, kaydedilen pivot hâlâ Sheet1.A2:E30 adresini gösterir ve bir sonraki yenilemede bozuk aralığı bildirme işi tüketiciye kalır. Bir sıralama uyarısı da buraya ait: HotXLS AutoFilter aralıklarını pivot fragmanından sonra <table:database-ranges> olarak yazıyor ve corpus örneği hiçbir database range taşımıyor; bu yüzden hem filtre hem pivot içeren bir workbook'u, bu iki öğenin göreli sırasına güvenmeden önce bir ODF şema doğrulayıcıdan geçirmek gerekir

Yalnızca corpus örneğiyle değil, kendi üreticinizin dosyalarıyla da test edin. Namespace taşıma, bir üreticinin üst öğede bildirdiği her prefix'i karşılar; ama prefix'i pivot öğesinin kendisinde bildiren ya da table sözlüğü için varsayılan namespace kullanan bir belge, LibreOffice örneğinin tetiklemediği atlama ve gölgeleme dallarını çalıştırır. İkisi de uygulandı; ikisinin de corpus'ta henüz bir örneği yok ve bu ayrım tam da bir changelog girdisinin bulanıklaştırmaya meyilli olduğu türden bir şey

v2.382.0'daki birebir data pilot yakalaması ve v2.382.1'deki namespace kapsamı düzeltmesi güncel HotXLS Delphi Excel Component içinde geliyor; ürün sayfası Delphi ve C++Builder için tam ODS, XLSX ve XLS okuma-yazma kapsamını listeliyor