XML ve JSON İşlemede Yeni Bir Yöntem-Declarative Stream Mapping (DSM)

Mehmet Fatih Ercik
Codable
Published in
10 min readJul 16, 2019

Bu yazıda, açık kaynak olarak yayınladığım XML ve JSON işlemeyi(parse) kolaylaştıran(Özellikle karmaşık dosyalar için) Declarative Stream Mapping (DSM) kütüphanesinden bahsedeceğim. Öncelikle çok kısaca DSM’in ne olduğunu ve DSM kullanılarak neler yapılabileceğine anlattıktan sonra, XML ve JSON formatındaki veriyi işlerken bu güne kadar kulanıdğımız klasik yöntemlerinden ve bu yöntemleri kullanırken karşılaştığımız problemelerden bahsedip; bu yeni kütüphanenin bu problemlere nasıl çözümler getirdiğini anlatacağım. Son olarakta kütüphanenin kolay kullanımını sağlayan bazı özelliklerine kısaca değineceğim. Yazı biraz uzun oldu şimdiden iyi okumalar.

Kütüphanenin dokümantasyonuna ve spesifikasyonuna buradan ulaşabilirsiniz.

Haydi başlayalım !!!

Declarative Stream Mapping (DSM)’e Giriş

DSM esasında ihtiyaçtan doğan bir kütüphane oldu diyebiliriz. Finans dünyasında iyi bilinen, murex sisteminin entegrasyonu yapılırken karşılaşılan problemlere bir çözüm olarak geliştirilmiş dört yıllık bir AR-GE’nin sonucudur. Murex sisteminden gelen karmaşık XML mesajlarının daha kolay işlenmesi için geliştirilen DSM, yazılım geliştirme, bakım ve kod performansı konusunda işimizi çok kolaylaştırdı.

DSM, XML veya JSON dokümanını belleğe yüklemeden stream olarak, daha kolay işlenmesine olanak sağlar. DSM, YAML formatında tanımlanmış mapping tanımları ile kaynak dosyadaki, sadece istenilen dataları işleyerek gereksiz CPU ve bellek kullanımının önüne geçer.

Dosya işleme sırasında, filtreleme, transformasyon, gruplama, birleştirme, script çalıştırma(Javascript, Groovy, Jexl), fonksiyon çağırma gibi daha bir çok işlem yapmaya olanak sağlar. Bunları aşağıda detaylı anlatacağım

DSM, bir bakıma XSLT görevi görür fakat XSLT den artısı veriyi Stream olarak yani belleğe yüklemeden işler. Birde hem JSON hem de XML için çalıştığını da eklemek gerekir.

Klasik Doküman(XML, JSON) İşleme Yöntemleri

XML ve JSON formatlarını işlemek için birçok yöntem olsa da temel de aşağıdaki şekilde gruplaya biliriz.

Deserialization(Unmarshalling)

Deserialization dediğimiz bu yöntem esasında, kaynak dokümanla aynı yapıda olacak şekilde class’lar oluşturularak, dokümanın direkt bu class’lara dönüştürülmesi ile gerçekleşir. Kaynak dosya ile class’lar arasındaki mapping işlemi genelde annotasyonlar kullanılarak yapılır. En çok tercih edilen yöntemlerden biridir.

Dosya yapısı karmaşıklaştıkça kodlaması zorlaşır, performans düşer. Oluşturulan class’larda çok fazla annotasyon olduğu için kod okunurluğu düşer. Kaynak dosyanın tamamı belleğe alındığı için dosya boyutu fazla ise bellek problemleri ortaya çıkar.

Genelde uygulamamızda kullandığımız class’lar(Application Class) ile deserialize için kullandığımız class’lar(Stub class’lar) faklı olur. Bu tür durumlarda stub objelerin, uygulama objelerine eşleştirilmesi gerekmektedir. Buda sistemde ekstra karmaşıklığa sebep olur.

Deserialization yöntemi ile JSON veya XML datanın nasıl işlendiğini aşağıdaki diyagramdan inceleyebilirsiniz.

Deserialization(Unmarshalling)

DOM( Document Object Model) Parsing

Bu yöntemde kaynak dosya işlendikten sonra genel objelere (Document, Element,Node,JsonObject,JsonArray vb) dönüştürülerek bellekte tutulur. Kaynak dosyada istenilen bilgilere bu genel objeler üzerinden ulaşılır.

Bu yöntemle, sadece işlenmek istenilen alanlar kaynak dosyadan alınabilir. Bütün veri bellekte tutulduğu için kaynak dosyanın herhangi bir yerindeki bilgiyle erişilebilir. Yine bu yöntemle birlikte XPath ve JsonPath kullanılarak kaynak dosyadaki belirli kurallara uyan bilgilere erişilebilir.

Kullanımı deserialization yöntemine göre daha zorduır. Bu yöntemde yazılan kod daha karmaşık ve okunurluğu daha azdır. Dosya yapısı karmaşıklaştıkça kodlanması içinden çıkılamaz bir hal alabilir. Kaynak dosyanın tamamı belleğe alındığı için dosya boyutu fazla ise bellek problemleri ortaya çıkar. Boyutu fazla olan dokumalar için kullanılmamalıdır.

DOM Parsing yöntemi ile JSON veya XML datanın nasıl işlendiğini aşağıdaki diyagramdan inceleyebilirsiniz.

Dom Parsing

Stream Parsing

Bu yöntemde parsing işlemi, kaynak dosya(XML, JSON) okunurken yapılır. Dosya baştan sona doğru sırası ile bütün tagların okunması ile gerçekleşir. Bütün dosya belleğe yüklenmez. Önceki yöntemlere göre daha performanslıdır. Sadece istenilen bilgiler dosyadan alınır. Boyutu fazla olan dosyalar için genelde bu yöntem kullanılır.

Önceki iki yönteme göre kullanımı daha zordur. Data bellekte tutulmadığı için sadece o anda okunulan bilgiye erişilebilir. Yani dokümanın başı okunurken, dokümanın sonundaki bilgiye erişilemez. Dosya yapısı karmaşıklaştıkça kodlanması çok zorlaşır.

DSM’in Konumu

DSM kütüphanesi yukarıda özetlenen yöntemlerden Deserialization(Unmarshalling) ve Stream Parsing yöntemlerinin birleşiminden oluşur. Stream parsing yönteminin sağladığı performans ve bellek, deserialization yönteminin sağladığı kolaylıklarla birleştirir. Aşağıdaki şekilde özetlediği gibi kaynak dosya ile uygulama class’ları arasındaki YAML veya JSON formatında tanımlanan mapping tanımları ile veri direkt uygulama objelerine dönüştürülür. Arada ekstra bir katman barındırmaz.

Mapping dosyası kaynak dosyanın nasıl işleneceğini belirleyen kuralları içerir. Bu kurallar temelde uygulama class’larındaki bir alanın kaynak dosyadaki hangi tagdan alınacağını belirler.

Problemler ve DSM’in Sağladığı Çözüm Yöntemleri

Aşağıda genel manada dosya okunurken karşılaşılan problemler ve DSM kütüphanesinin sağladığı çözümlerden bahsedeceğim.

Memory ve CPU

Bir dosyanın bellekte kapladığı boyut dosyanın bilinen boyutundan çok daha fazladır. Dolayısı ile dosyanın boyutu bellek boyutundan az olsa bile belleğe alındığında daha fazla yer kaplar ve OutOfMemory hatalarına sebep olabilir. Deserialization ve DOM parsing yöntemlerinde bütün veri belleğe alındığımdan büyük doyalar için bellek problemleri oluşur.

DSM kaynak dosyayı stream şekillinde işlediği için dosyanın boyutu önemli değildir. Sadece belirlenen koşullara uyan veri kaynak dosyadan okunur. Dolayısı ile gereksiz veri işlenmediği için hem CPU hem de Memory kullanımı düşer.

Örneğin aşağıdaki formatta veri içeren 10 GB bir XML dosyasını işlediğimizi farz edelim. Bu dosyayı Deserialization ve DOM yöntemleri kullanarak işlendiğinde hem performans hem de bellek problemleri ortaya çıkabilir.

DSM kullanılarak aşağıdaki şekilde işlenebilir.

XML File:

Mapping File:

XML dosyası, aşağıdaki tanımlar yapılarak işlenir. function alanında processData fonksiyonu tanımlanarak her item tagı için bu fonksiyonun çağırılması sağlanır. registerDate alanı, XML dosyasındaki date tagındaki değeri alması için mapping tanımında path alanı kullanılır. registerDate alanının java.util.Date tipine dönüştürülmesi için dateFormat tanımlanır. id ve name alanlarının isimleri XML deki tag isimleri ile aynı olduğu için, ayrıca path alanı kullanmaya gerek yoktur.

mapping.yaml

processData Fonksiyonu:

Yukarıdaki JSON dizisinin her bir elemanı için aşağıda tanımlanan processData fonksiyon çalıştırılır. Bu fonksiyonda istenilen işlemler(servis, veritabanı vb ) yapılabilir. Bütün bu işlemler dosya okunurken olur.

Java Kodu:

Java Code

Sonuç:

DSM okunan dosyanın parça parça işlenmesine olanak sağlar. Bu örnekte her bir item tagı için processData fonksiyonu çalıştırılır. processData fonksiyonunun çalışması ile terminalde aşağıdaki gibi bir çıktı oluşur

{id=1, name=Mehmet, registerDate=Sat Jun 15 00:00:00 EET 2019}
{id=2, name=Fatih, registerDate=Thu Jun 13 00:00:00 EET 2019}
{id=3, name=Ercik, registerDate=Tue Jun 11 00:00:00 EET 2019}

Farklı Yapıda Kaynak Dosyalar

Farklı sistemden gelen farklı yapıdaki XML veya JSON formatındaki dosya aynı uygulama class’larına dönüştürülmek istendiğinde, her farklı kaynak için ayrı ayrı parse edilmesi gerekir. Bu da kaynak dosya sayısı arttıkça zorlaşan ve karmaşıklaşan bir durum ortaya çıkarır. Deserialization yöntemi kullanıldığında her bir kaynak için bir çok stub class’ı oluşturulur. Bunun yanında oluşturulan stub class’lardan uygulama class’larına dönüşüm ekstra iş yükü çıkarır. DSM mapping tanımlarını ayrı bir dosyada tuttuğu için stub class’larının üretilmesine ve uygulama sınıflarında değişiklik yapılmasına gerek kalmaz.

DSM bu probleme iki faklı çözüm sunar. Birincisi yöntemi, her bir kaynak dosya için faklı mapping tanımları yapılarak, uygulama sınıflarında herhangi bir değişiklik yapılamadan parsing yapılması şeklinde özetleye biliriz. Özellikle karmaşık dosyalar için bu yöntemin kullanılması daha iyidir.

İkinci yöntem ise aynı alan için birçok path tanımı yapılarak sadece bir mapping dosyası ile bütün parsing işlemi yapılabilir.

Aşağıda bir XML diğeri JSON olan iki faklı kaynak dosyasının tek dosyada tanımlanan mapping tanımları ile nasıl parse edildiği gösterilmektedir.

system1.xml (XML):

w3schools

system2.json (JSON):

Mapping File

system1.xml XML dosyasında attribute olarak tanımlanan category alanın okunması için mapping dosyasında bu alan için attribute :true olarak tanımlanır. XML’de array olarak tanımlanan author tagı, yine mapping dosyasında type’ı array olarak tanımlanır. published alanı XML’deki year tagından okunarak java.util.Date objesine çevrilebilmesi için dataTypeParams alanında dateFormat yyyy olarak tanımlanır. subTitle alanı XML’deki title tagından okunur.

system2.json dosyasında görüldüğü gibi ikinci dosya JSON formatında ve dosyanın yapısı(structure) ilk dosyadan farklıdır. Dosyadaki author alanı string olmasına rağmen array olarak okunması için tanımlama yapılır. category alanı dosyada olmadığı için default olarak ‘software’ atanması sağlanır. published alanı date objesine dönüştürülecek şekilde tanım yapılır.

Her iki kaynak dosyanın okunması için result tagının altına bir den fazla tanım eklenmesi gerekir. processBook fonksiyonu her iki tanım içine eklenir. Aşağıda mapping dosyasının içeriğini detaylı incelemeniz konunun daha iyi anlaşılmasına yardımcı olacaktır.

Java Kodu:

Sonuç:

Aynı dosyada farklı yapıdaki ve formattaki(JSON, XML) dosyaları sadece bir mapping dosyası ile işlenebilir. Aşağıda görüldüğü gibi iki çıkınında yapısı(structure) aynıdır.

Filtreleme

Genellikle, kaynak dosyada belirli şartlara uyan verileri okumak için belirli kuralları içeren filtrelerden geçirmemiz gerekir.

DSM, dosya okunurken filtreleme olanağı sağlar. Dolayısı ile gereksiz veri işlenmesinin önüne geçer.

Örneğin aşağıdaki XML dosyasında, sadece category’si web olan kayıtlar ile ilgilendiğimizi düşünelim.

Data filtrelemek için, bir expression içeren filter alanı kullanılır. Bu expression , Jexl, Groovy veya JavaScript olarak yazılabilir.

Aşağıdaki mapping tanımında category alanı sadece web olan kayıtlar alınır.

filter alanı herhangi bir tanım üzerinde kullanılabilir. Örneğin price alanı 50'den düşük olan kayıtlar için price alanı default olarak 50 olmasını istediğimizde aşağıdaki gibi bir tanım yapmamız yeterlidir.

Sonuç:

filter alanı kullanılarak kaynak dosyadaki sadece belirlediğimiz koşulları sağlayan dataları alıp işleyebiliriz. Yukarıdaki örnekte category alanı ‘web’ olan kayıt okundu. Bu kaydın price alanı 50 den küçük olduğu için(49.99) default olarak 50 atandı. İşlenmiş data konsola yazıldığında aşağıdaki gibi bir çıktı oluşur.

{category=web, title=XQuery Kick Start, authors=[James McGovern, Per Bothner, Kurt Cagle, James Linn, Vaidyanathan Nagarajan], price=50.0}

Kaynak Dosyada Arama

Tıpkı veri tabanında SQL ile yaptığımız sorgu gibi kaynak dosyada belirlediğimiz kurallara uyan bilgileri sorgulayabiliriz. Örneğin aşağıdaki JSON verisi için title alanında belirlediğimiz değerleri içeren kayıtları almak istiyoruz.

DSM dışardan parametre verilerek filter alanında bu parametrelerin kullanılmasına olanak sağlar. Aşağıdaki mapping tanımında görüldüğü üzere filter alanında params objesi üzerinden dışardan verilen parametrelere erişilebilir.

Aşağıdaki şekilde dışarıdan getParams() fonksiyonu kullanılarak title parametresi verilir..

Sonuç:

JSON dosyasındaki iki kayıttan sadece biri title alanında JavaScript kelimesi içerir. Dolaysıyla aşağıdaki çıktıda görüldüğü gibi sadece JavaScript kelimesi içeren kayıt alınmıştır.

{isbn=9781449365035, title=Speaking JavaScript, authors=[Axel Rauschmayer], published=Sat Feb 01 00:00:00 EET 2014, category=software}

Değer Normalisasyonu

Bazen kaynak dosyadan okuduğumuz bir değer bizim istediğimiz gibi olmayabilir. String değerinin içerisinde istediğimiz bilgileri almak için kod yazmamız gerekir. Örneğin Aşağıdaki XML dosyasında href(#contract_1127473) alanını okurken sadece sayı kısmını (1127473) almak için bir normalizasyon yapmamamız gerekiyor. DSM, script çalıştırabildiği için bu işlemi yapmak çok kolay olacaktır.

Aşağıda gösterildiği gibi mapping dosyasında id alanı href attribute’ünden alınıp normalize tanımında ”#contract_” kısmı replace fonksiyonu kullanılarak silinmiştir.

Değer Transformasyonu Yapma

Kaynak dosyadan okuduğumuz bir değerin karşılığı kendi sistemimizde çok farklı olabilir. Bu tarz durumlarda dosyadan okuduğumuz değeri kendi sistemimizdeki karşılığına dönüştürmemiz gerekiyor. Örneğin aşağıdaki XML dosyasında country_code tagı her hangi bir ülke kodu olabilir. Ama biz kendi sistemimizde ülkeleri kodları ile değil isimleri ile tutmamız gerektiğinde bu ülke kodları ile ülke isimleri arasında bir lookup tablosu oluşturmamız gerekiyor. Birde sadece belirli ülkelerin isimlerini çevirirken geriye kalanları OTHER diye tanımlama ihtiyacımız da olabilir.

Aşağıdaki mapping dosyasında görüldüğü gibi countryName alanını country_code tagından okunuyor. Ülke kodundan ülke adını bulmak için COUNTRY_CODE_TO_NAM transformation tanımı transformationCode alanında verilerek ülke kodundan ülke ismi bulunuyor. rating alanı kaynak dosyada 1–10 arasında iken RATING transformation tanımı kullanılarak 1–5 arasına gelecek şekilde dönüştürülüyor.

Sonuç:

Değer transformasyonu DSM’in en güzel özelliklerinden biri. Bir bakıma switch-case görevi görüyor. Aynı şekilde bir çok if else tanımı yazmaktansa transformation kullanılarak gerekli dönüşümler yapılabilir. Yukarıdaki örnekte ülke kodundan ülke ismi bulunuyor ve countryName alanına bu bulunan değer atanıyor. Eğer transformation tanımında bu kod yoksa countryName alanına “Other” değerini atıyor. Aşağıdaki konsol çıktısında görüldüğü gibi transformasyon tanımında bulunmayan ülke kodları için “Other” atılmış.

[{countryName=Other, rating=3}, {countryName=Other, rating=5}, {countryName=Turkey, rating=5}, {countryName=France, rating=2}]

Default Değer Atama

Kaynak dosyada beklediğimiz bir tag veya değer olmadığında default olarak bir değer atama ihtiyacı olabilir. Yada bazı koşullara göre bir değer atamamız gerekebiyor. DSM default alanı kullanılarak varsayılan değer atama işlemini çok kolay bir şekilde çözüyor. default alanında script yazılarak daha karmaşık değer ataması yapmasına da olanak sağlıyor. default alanındaki değerin script olduğunu belirtmek için $ başlaması yeterlidir.

Örneğin aşağıdaki XML dosyasında category tagı olmadığında default olarak “other” atmak istiyoruz. Ayrıca discountPercentage alanınıda hesaplamak istiyoruz.

Aşağıdaki mapping dosyasında category alanı olmadığında default olarak “other” atamak için category tanımında default alanına “other” yazmamız yeterli.

Ayrıca discountPercentage alanı, price ve discount_price alanlarının bir biri ile oranlarından hesaplanması için default alanında, bu hesaplamayı yapan exspression yazılır.

Sonuç:

{category=children, title=Harry Potter, authors=[J K. Rowling], price=30.0, discountPrice=27, discountpercentage=0.1}{title=XQuery Kick Start, authors=[James McGovern, Per Bothner, Kurt Cagle, James Linn, Vaidyanathan Nagarajan], price=50.0, discountPrice=25, category=other, discountpercentage=0.5}

DSM’in Kullanımını Kolaylaştıran Özellikler

Inheritance(Kalıtım)

DSM de kullanılan Mapping dosyaları büyüdükçe yönetimi zorlaşmaya başlar. Mapping dosyalarını daha kolay yönetmek için mapping tanımlarını farklı dosyalara koyarak bunları daha sonra tek bir yerde birleştirebiliriz. Bir mapping dosyasında bulunan bir tanım başka bir mapping dosyasında bulunan tanımı kalıtarak farklı özellikler ekleye bilir veya yazılan bir özelliği override edebilir.

Örneğin Aşağıdaki Mapping dosyasında transformation tanımları farklı bir dosyaya konularak daha anlaşılır bir hale getirildi.

transformation.yaml

Aşağıdaki main.yaml dosyasında $extendsalanı kullanılarak transformation.yaml dosyasına extend edildi.

transformation.yaml dosyasındaki bütün tanımlar main.yaml dosyasındaki tanımlar ile birleştirilir. extends alanı kullanılarak bir çok dosyaya birden extend edilebilir. Yani multi inheritance destekleniyor.

main.yaml

Fragmentation(Parçalama)

Sürekli tekrar eden tanımları ortadan kaldırıp bunları tek bir parça halinde kullanmak için fragments alanı kullanılabilir. Bu sayede tekrar eden tanımlar tek bir yerde toplanır ve mapping dosyasının boyutu bir hayli düşer. Özellikle değiştirilmek istenen bir alan var ise override edilebilir.

Örneğin aşağıdaki fragments alanında tanımlanan product fragmenti mainProduct ve productList alanlarından $ref alanı kullanılarak referans alınmıştır. mainProduct için, price tanımın path alanı override edilerek discount_price alanından alınmıştır.

Default DataType Parametreleri

DSM’ de data tipi dönüştürmeleri TypeConverter interface’i implement edilerek yapılabilir. Tip dönüşümleri temelde bir string objesini alıp istenilen tipe dönüştürülmesi ile olur. Bu dönüşümler yapılırken tip dönüşümü için bazı parametrelere ihtiyaç duyarlar. Örneğin date tipine dönüşüm için dateFormat parametresinin verilmesi gerekir.

Örneğin aşağıdaki mapping dosyasında published alanı java.util.Date tipine dönüştürülmesi için dataType tanımı date olarak verilmiştir. Date objesine dönüştürmemiz için dataTypeParams alanı üzerinden dateFormat’ının nasıl olacağını belirtiyoruz.

Bütün date objeleri için bu tanımı yapmak yerine dateFormat tanımını params alanının içine tanımlayarak bütün date objeleri için aynı formatı kullanmasını sağlaya biliriz. Varsayılanın dışına çıkmamız gereken tanımlarda yine dataTypeParams alanını kullanabiliriz. Bu şekildeki bir kullanımda tanım aşağıdaki gibi olacaktır.

Regular Expression Path

DSM’de kullanılmak istenen tagın yolunu belirtmek için path alanı kullanılır. path alanı regex bir ifade içerir. Bu regex ifade kaynak dosyada geçen bütün absolutePath değerleri için çalışır. Örneğin path alanı

/.+book

olarak tanımlandığında aşağıdaki bütün değerleri kapsar.

/feed/books/book
/books/book
/foo/bar/books/book

Sonuç olarak

DSM, XML ve JSON işleme konusunda farklı bir bakış açısı getiriyor. Dosyanın nasıl işleneceğini ve çıktının yapısının nasıl olacağını tamamen bizim kontrolümüze veriyor. Yapmamız gereken kullanmak istediğimiz alanın yolunu vermek. Gerisini DSM kendisi halledip istediğimiz değeri getiriyor.

Kodun derlenmesine ihtiyaç duymadan çalışması yanında dosyanın nasıl işleneceğini çok basit tanımlamalar ile tamamen bizim kontrolümüze vermesi development sürecini hızlandırıyor. Özellikle karmaşık dosyaların işlenmesinde hem development hem de kod performansını artırıyor.

DSM kütüphanesinin gelişimi sizin katkılarınız ve kullanımınız ile olur. Hem dokümantasyon hem de developement için her türlü desteğiniz çok değerlidir.

Şu an yayınlanan spesifikasyon sadece Java dili için geliştirildi. Bunun diğer programlama dilleri için geliştirilmesi konusunda katkıda bulunmak isteyen olursa çok memnun olurum.

--

--