Nastal čas na kakao - Aplikace RSS: parsování XML - MujMAC.cz - Apple, Mac OS X, Apple iPod

Odběr fotomagazínu

Fotografický magazín "iZIN IDIF" každý týden ve Vašem e-mailu.
Co nového ve světě fotografie!

 

Zadejte Vaši e-mailovou adresu:

Kamarád fotí rád?

Přihlas ho k odběru fotomagazínu!

 

Zadejte e-mailovou adresu kamaráda:

Soutěž

Sponzorem soutěže je:

IDIF

 

Jaký fotograf/ka získal/a cenu za nejpopulárnější příspěvek v Nikon Photo Contest?

V dnešní soutěži hrajeme o:

Seriály

Více seriálů



Informace

Nastal čas na kakao - Aplikace RSS: parsování XML

21. října 2004, 00.00 | Předminule jsme probrali strukturu datového modelu naší aplikace a rozhraní třídy RSS; v minulém dílu jsme si ukázali její implementaci – inicializaci, standardní metodu dealloc, a accesory. Zbývá nám tedy ještě ukázat si, jak je implementováno parsování XML, a pak se už budeme moci pustit do tvorby grafického uživatelského rozhraní.

Předminule jsme probrali strukturu datového modelu naší aplikace a rozhraní třídy RSS; v minulém dílu jsme si ukázali její implementaci – inicializaci, standardní metodu dealloc, a accesory. Zbývá nám tedy ještě ukázat si, jak je implementováno parsování XML, a pak se už budeme moci pustit do tvorby grafického uživatelského rozhraní. (Znovu se možná vyplatí zdůraznit, že toto pořadí prací jsme si volně vybrali – Cocoa umožňuje to dělat třeba naopak, nejprve sestavit GUI a až potom se pustit do datového modelu, nebo jakkoli jinak nám to v konkrétním projektu vyhovuje.)

Parsování XML má dvě části: součástí implementace třídy RSS jsou metody delegáta třídy NSXMLParser, jež už vlastně známe z prvního dílu našeho seriálu. Ty ovšem jen předávají požadavky objektům NSMutableDictionary, které se starají o vlastní parsování.

Stojí za to si znovu připomenout díl "Podtřídy, delegáti, vkládání...": tehdy jsme si řekli, že v Cocoa poměrně často užíváme jiných technik než vytváření podtříd, neboť si tak ušetříme práci. Nejinak tomu je i v tomto projektu: místo toho, abychom implementovali poměrně složité vlastní třídy pro datovou strukturu odpovídající hierarchické struktuře XML, prostě použijeme standardní knihovní kontejnery NSMutableDictionary a NSMutableArray, a potřebné accesory a speciální služby doplníme do třídy NSMutableDictionary prostřednictvím kategorie.

Metody delegáta

Dnes už bychom měli principu delegace rozumět mnohem lépe, než v prvém dílu: víme, že řada objektů Cocoa využívá delegátů – spolupracujících objektů, jež mohou implementovat některé z řady standardních metod. Nejinak tomu je i s třídou NSXMLParser: její instance při parsování dat ve formátu XML posílá svému delegátu zprávy pro každý prvek XML, na který narazí.

Nás pro naše účely zajímají pouze tři zprávy: informace o otvíracím tagu, informace o uzavíracím tagu, a informace o textovém obsahu. Ostatní metody tedy vůbec nebudeme implementovat; víme, že mechanismus delegace se standardně využívá tak, že řídící objekt posílá svému delegátu pouze ty zprávy, pro něž má delegát vhodné metody.

Vlastní implementace metod delegáta ve třídě RSS je triviální – prostě si vyžádáme zpracování odpovídající události od aktuální instance NSMutableDictionary (již objekt třídy RSS, jak víme od minula, udržuje v pomocné proměnné md). Metody, jež zpracovávají události "otevření tagu" a "uzavření tagu" navíc obě vracejí novou hodnotu, již dosazujeme zpět do md; to proto, že obě mohou měnit aktuální instanci NSMutableDictionary (např. otvírací tag může vytvořit vnořený objekt, je-li to zapotřebí). Celá implementace pak vypadá takto:

-(void)parser:p didStartElement:element namespaceURI:u qualifiedName:q attributes:a {
    md=[md ocs_startElement:element];
}
-(void)parser:p didEndElement:e namespaceURI:u qualifiedName:q {
    md=[md ocs_endElement];
}
-(void)parser:p foundCharacters:string {
    [md ocs_stringData:string];
}

Metody třídy NSMutableDictionary

Pro representaci dat ve formátu XML (odpovídajících v našem případě novinkám ze zdroje RSS) potřebujeme objekty, jež budou schopny obsahovat libovolná data pro dané textové klíče (jako NSMutableDictionary), v případě stejných klíčů vícenásobná (tedy jako NSMutableArray), ale přitom rozumějí našim zprávám ocs_startElement:, ocs_endElement a ocs_stringData: tak, jak potřebujeme. Ve většině objektových systémů by to znamenalo nutnou implementaci poměrně složité nové třídy; ne tak v Cocoa: zde prostě přímo použijeme NSMutableDictionary a NSMutableArray, a všechny potřebné nové služby doplníme pomocí kategorií.

Přidáme do projektu nový zdrojový soubor "XMLImportCategory.m" (a odpovídající hlavičkový soubor "XMLImportCategory.h"). Samozřejmě nezapomeneme přidat na začátek zdrojového souboru "RSS.m" direktivu #import "XMLImportCategory.h"; obsah tohoto hlavičkového souboru bude vypadat zhruba takto:

@interface NSMutableDictionary (XMLImportCategory)
-(NSString*)ocs_currentElement;
-(void)ocs_setCurrentElement:(NSString*)element;
-(NSMutableDictionary*)ocs_backLink;
-(void)ocs_nest:obj forKey:(NSString*)key;
-(NSMutableDictionary*)ocs_startElement:(NSString*)element;
-(NSMutableDictionary*)ocs_endElement;
-(void)ocs_stringData:(NSString*)string;
@end

První dvojice zpráv – vlastně jde o zcela standardní accesory – zajistí standardní přístup k "aktuálnímu elementu". Ten budeme potřebovat při parsování, proto, že zprávy o otevření nového tagu a o jeho obsahu přicházejí zvlášť – např. titulek "BBC" tedy dostaneme jako trojici zpráv: (i) otevření tagu "title", (ii) textový obsah "BBC", (iii) uzavření tagu. Musíme si tedy při otevření tagu zapamatovat jméno aktuálního elementu, a při přijetí textového obsahu jej použít.

Následující zpráva ocs_backLink zajišťuje také technickou pomůcku: ve struktuře vzájemně vnořených objektů potřebujeme, aby vnořený objekt obsahoval odkaz na objekt o úroveň výš: to proto, že když plnění vnořeného objektu dokončíme (tj. přijde zpráva o odpovídajícím uzavření tagu), musíme se opět "vrátit" o úroveň výš. Zpráva tedy vrátí nadřízený objekt (pokud takový existuje).

Nejsložitější je čtvrtá zpráva, ocs_nest:forKey: – jak uvidíme níže, budeme na implementaci odpovídající metody potřebovat celých patnáct řádků Objective C. Zpráva zajišťuje základní službu při sestavování struktury XML: do objektu, jenž ji přijme, vloží vnořený objekt. Přitom zajistí potřebné vedlejší funkce:

  • kdykoli se vkládá NSMutableDictionary, nastaví se v něm korektně odkaz na nadřízený objekt (aby správně fungovala metoda ocs_backLink);
  • pokud se vkládá string s tagem, pro nějž už v objektu nějaký string je, oba texty se zřetězí;
  • pokud se vkládá NSMutableDictionary s tagem, pro nějž už v objektu je string, string se zapomene. To proto, že NSXMLParser nám jako textové objekty předává i oddělovače mezi tagy (např. konce řádků);
  • z téhož důvodu naopak, pokud se vkládá string s tagem, pro nějž už v objektu je NSMutableDictionary, nic se nevloží;
  • konečně, vkládáme-li NSMutableDictionary s tagem, pro nějž už v objektu je jiný NSMutableDictionary, vytvoří se na jejich nový objekt třídy NSMutableArray, obsahující oba dva (resp. všechny, je-li jich ještě více).

Poslední trojice zpráv pak již využije toto rozhraní k poměrně snadné implementaci služeb, potřebných pro třídu RSS – otavření nového tagu (ocs_startElement:), vložení textových dat (ocs_stringData:) a uzavření tagu (ocs_endElement).

Implementace kategorie

Jak je z výše uvedeného rozboru vidět, stačí nám jediná kategorie třídy NSMutableDictionary (pokud by ovšem úloha byla složitější, samozřejmě bychom pomocí dalších kategorií mohli potřebným způsobem rozšířit i služby tříd NSMutableArray, případně NSString). Ukážeme si implementaci jednotlivých metod postupně, a vždy si podrobněji vysvětlíme použitou techniku.

-(NSString*)ocs_currentElement {
    return [self objectForKey:@"."];
}
-(void)ocs_setCurrentElement:(NSString*)element {
    if (element) [self setObject:element forKey:@"."];
    else [self removeObjectForKey:@"."];
}

Accesory pro aktuální element jsou triviální: element je prostě uložen do objektu pod klíčem ".".

-(NSMutableDictionary*)ocs_backLink { // trick so as the buggy description works!
    return [[self objectForKey:@"<"] nonretainedObjectValue];
}

Podobně odkaz na nadřízený objekt je uložen pod klíčem "<". Je zde však použit malý trik: jde o to, že pokud bychom ukládali odkaz na nadřízený objekt přímo (a pak jej také přímo vraceli příkazem return [self objectForKey:@"<"]), vznikl by v datové struktuře cyklus (nadřízený objekt A obsahuje pod nějakým klíčem K vnořený objekt B, ten obsahuje pod klíčem "<" nadřízený objekt A, ten obsahuje pod klíčem K objekt B, ten...). Z hlediska vlastního kódu i samotné třídy NSMutableDictionary by to nebyl velký problém; vzpomeňme si však na díl "Vznik a zánik objektů", kde jsme si vysvětlili, že a proč cykly mohou vést k tomu, že se nějaký objekt nikdy neuvolní – pokud NSMutableDictionary používá pro udržení odkazů na vložené objekty retain (a tak tomu skutečně je).

Abychom se tomu vyhnuli, namísto přímého vložení odkazu na nadřízený objekt proto vložíme jeho adresu nepřímo, zakódovanou do samostatného objektu třídy NSValue (viz níže, valueWithNonretainedObject:). Zpět původní hodnotu (tedy přímo adresu nadřízeného objektu) dostaneme právě zprávou nonretainedObjectValue.

-(void)ocs_nest:obj forKey:(NSString*)key {
    if (!obj || !key) return; // stay on the safe side
    BOOL newIsDict=[obj isKindOfClass:[NSMutableDictionary class]];
    id alreadyThere=[self objectForKey:key];
    if (newIsDict) [obj setObject:[NSValue valueWithNonretainedObject:self] forKey:@"<"];
    if (!alreadyThere) [self setObject:obj forKey:key]; // new value
    else if ([alreadyThere isKindOfClass:[NSString class]]) // override or concat
        if (newIsDict) [self setObject:obj forKey:key]; // override
        else [self setObject:[alreadyThere stringByAppendingString:obj] forKey:key];
    else if (newIsDict) // if somtehing's already there, only dict gets added
        if ([alreadyThere isKindOfClass:[NSMutableDictionary class]]) // 2nd object
            [self setObject:[NSMutableArray arrayWithObjects:alreadyThere,obj,nil] forKey:key];
        else if ([alreadyThere isKindOfClass:[NSMutableArray class]]) // 3rd or more
            [alreadyThere addObject:obj];
}

Metoda ocs_nest:forKey: implementuje hlavní funkčnost celého sestavování struktury kontejnerů, jak jsme si ji popsali v bodech na konci minulého odstavce. Podívejme se na ni řádek po řádku:

  • nejprve se jen postaráme o to, aby – pokud někdo zprávu pošle bez hodnoty obj nebo key – se nic nestalo;
  • pak si v pomocné proměnné newIsDict zapamatujeme, zda je vkládaný objekt instancí třídy NSMutableDictionary – vzpomínáte ještě na díl "Základní služby objektů", odstavec "Hierarchie tříd"?
  • do další pomocné proměnné alreadyThere uložíme objekt, který již pro daný key máme (existuje-li takový);
  • pokud je vkládaný objekt instancí třídy NSMutableDictionary, uložíme do něj odkaz na nadřízený objekt (ohledně důvodu použití třídy NSValue viz diskusi implementace metody ocs_backLink výše);
  • pokud jsme pro daný key nic neměli, prostě uložíme nový objekt;
  • je-li v alreadyThere text, nový text za něj připojíme, nebo jej nahradíme novým NSMutableDictionary;
  • jinak pokračujeme jen je-li nový objekt NSMutableDictionary (kdyby to byl text, chceme jej ignorovat);
  • pokud byl v alreadyThere také NSMutableDictionary, vytvoříme NSMutableArray s oběma;
  • jinak musí být alreadyThere pole objektů, a nový prostě přidáme na konec.
-(NSMutableDictionary*)ocs_startElement:(NSString*)element {
    NSString *curr=[self ocs_currentElement];
    if (!curr) {
        [self ocs_setCurrentElement:element]; // if there was none, record it
        return self;
    } else { // otherwise has to nest
        NSMutableDictionary *md=[NSMutableDictionary dictionary];
        [self ocs_nest:md forKey:curr];
        [md ocs_setCurrentElement:element];
        return md;
    }
}

Při otvírání nového tagu se podíváme na ocs_currentElement, zda je právě nějaký tag otevřený. Pokud tomu tak není, prostě si ten, jenž právě přišel, zapamatujeme; jinak vytvoříme vnořený NSMutableDictionary a vrátíme jej (sestoupíme tedy o úroveň níže).

-(NSMutableDictionary*)ocs_endElement {
    NSMutableDictionary *new=[self ocs_currentElement]?self:[self ocs_backLink];
    [new ocs_setCurrentElement:nil]; // unset current
    return new; // if there was no current, drop back
}

Analogicky při zavírání tagu ověříme, zda je právě nějaký tag otevřený. Pokud tomu tak není, vystoupíme o úroveň výš (ocs_backLink). Pak jen zrušíme otevřený tag.

-(void)ocs_stringData:(NSString*)string {
    string=[string stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
    [self ocs_nest:string forKey:[self ocs_currentElement]];
}

Poslední metoda je nejjednodušší: jen odřízneme případné oddělovače na okrajích textu (stringByTrimmingCharactersInSet:), a pak použijeme metodu ocs_nest:forKey: pro vložení výsledku do aktuálního objektu.

A to je celé: kód, který jsme si ukázali dnes, převede data ve formátu XML na strukturu kontejnerů; kód, jenž jsme viděli minule, obsahuje accesory, jež z této struktury získají titulek a seznam novinek. Příště se tedy pustíme do přípravy grafického uživatelského rozhraní, jež tyto informace patřičným způsobem zobrazí.

Obsah seriálu (více o seriálu):

Tématické zařazení:

 » Rubriky  » Informace  

 » Rubriky  » Agregator  

 » Rubriky  » Začínáme s  

Diskuse k článku

 

Vložit nový příspěvek   Sbalit příspěvky

 

Zatím nebyl uložen žádný příspěvek, buďte první.

 

 

Vložit nový příspěvek

Jméno:

Pohlaví:

,

E-mail:

Předmět:

Příspěvek:

 

Kontrola:

Do spodního pole opište z obrázku 5 znaků:

Kód pro ověření

 

 

 

 

 

Přihlášení k mému účtu

Uživatelské jméno:

Heslo: