気楽なソフト工房

プログラミングについていろいろな記事を書いています。



mykonos2008

Author:mykonos2008
システムエンジニアとして働いている30代の会社員です。
仕事や趣味でプログラムを書いている方の役に立つ記事を書いていきたいと思っています。
ご意見、ご感想はこちらまで
If you are an english speaker,Please visit my english blog.

ついにsdkでますね。

http://m.cnet.com/news/google-to-launch-sdk-for-android-wearables-in-two-weeks/57620090
今回は「ChimeraXML」で配列型の要素を解析する方法を解説します。

以下のようなXMLがあります。

<goods>
    <goods-name>iPhone5S</goods-name>
    <categories>
        <category>
            <category-code>001</category-code>
            <category-name>telephone</category-name>
        </category>
        <category>
            <category-code>002</category-code>
            <category-name>music player</category-name>
        </category>
    </categories>
</goods>

[categories]要素は子要素として複数の[category]要素を持っています。
このような形式のXMLをデシリアライズする場合、[ChimeraXML]ではNSArray型のプロパティを使用します。

まず、先にNSArray型のプロパティに格納する[category]に対応するエンティティを定義します。

[CategoryEntity.h]
#import "CPXmlEntity.h"

@interface CategoryEntity : CPXmlEntity

@property(strong,nonatomic) NSString *categoryCode;
@property(strong,nonatomic) NSString *categoryName;

@end

[CategoryEntity.m]
#import "CategoryEntity.h"

@implementation CategoryEntity

+ (id)propertyInfoForElement:(NSString *)element
{
    static NSDictionary *propDic;
    
    if(!propDic){
        
        propDic = @{
                    @"category-code": @"categoryCode",
                    @"category-name": @"categoryName"
                    };
    }
    
    return propDic[element];
}

@end

[goods]要素に対応するGoodsEntityにはNSArray型のcategoriesプロパティを定義します。

[GoodsEntity.h]
#import "CPXmlEntity.h"

@interface GoodsEntity : CPXmlEntity

@property(strong,nonatomic) NSString *goodsName;
@property(strong,nonatomic) NSArray *categories;

@end

実装ファイルでは、categoriesプロパティの詳細情報を[propertyInfoForElement:]の戻り値として
解析エンジンに提供する必要があります。

[GoodsEntity.m]
#import "GoodsEntity.h"
#import "CategoryEntity.h"

@implementation GoodsEntity

+ (id)propertyInfoForElement:(NSString *)element
{
    static NSDictionary *propDic;
    
    if(!propDic){
        
        CPPropertyInfo *categories = [[CPPropertyInfo alloc] init];
        categories.name = @"categories";
        categories.childType = [CategoryEntity class];
        
        propDic = @{
                    @"goods-name": @"goodsName",
                    @"categories": categories
                    };
    }
    
    return propDic[element];
}

@end

[category]要素に対応するプロパティのメタ情報として、CPPropertyInfo型のオブジェクトを返却します。
CPPropertyInfoのnameプロパティには、プロパティ名を設定します。
そして、childTypeに、NSArrayに格納するオブジェクトの型を指定します。ここでは、CategoryEntity型を指定しています。

Objective-CにはJavaのようにジェネリックコレクションの仕組が無いため、childTypeにより、
解析エンジンに子要素の型情報を提供しているのです。

childTypeにはサンプルのようにCPXmlEntityクラスを継承するクラスの他、
NSStringとNSNumber型を指定することが出来ます。

その場合、解析対象のXMLは以下のような形になります。

<goods>
    <goods-name>iPhone5S</goods-name>
    <categories>
        <category-code>001</category-code>
        <category-code>002</category-code>
    </categories>
</goods>

解析を実行する処理はこれまでの例と同じです。

    NSData *data = [xmlText dataUsingEncoding:NSUTF8StringEncoding];
    ChimeraParser *chimeraParser = [[ChimeraParser alloc] initWithTargetClass:[GoodsEntity class]];
    GoodsEntity *goods = [chimeraParser parse:data];
    
    NSLog(@"goodsName = %@",goods.goodsName);
    for(id category in goods.categories) {
        NSLog(@"categoryCode = %@",[category categoryCode]);
        NSLog(@"categoryName = %@",[category categoryName]);
    }

NSArray型のプロパティには、実際にはNSMutableArray型のオブジェクトが設定されます。

暗黙的な配列の解析



以下のXMLを見てください。[goods]要素の子要素として、[category]要素が
複数回出現します。上記のサンプルとの違いは、[category]要素の上に
[categories]要素が存在しているかどうかです。

<goods>
    <goods-name>iPhone5S</goods-name>
    <category>
        <category-code>001</category-code>
        <category-name>telephone</category-name>
    </category>
    <category>
        <category-code>002</category-code>
        <category-name>music player</category-name>
    </category>
</goods>

上記のように、暗黙的な配列型要素を解析する場合、コードを少し変更する必要があります。

[GoodsEntity]の[propertyInfoForElement:]メソッドを以下のように変更します。

+ (id)propertyInfoForElement:(NSString *)element
{
    static NSDictionary *propDic;
    
    if(!propDic){
        
        CPPropertyInfo *categories = [[CPPropertyInfo alloc] init];
        categories.name = @"categories";
        categories.childType = [CategoryEntity class];
        categories.implicitArray = YES; //この行を追加
        
        propDic = @{
                    @"goods-name": @"goodsName",
                    //@"categories": categories
                    @"category": categories //対応する要素名を変更
                    };
    }
    
    return propDic[element];
}

CPPropertyInfoのimplicitArrayにYESを設定します。
また、要素名が[category]に対して、配列型のプロパティが対応するように変更しています。


今回はChimeraXMLで以下のようなXMLを解析する方法を説明します。

<image>
    <image-name>sun.jpg</image-name>
    <size>
        <width>250</width>
        <height>300</height>
    </size>
</image>

[image-name]要素については、前回のチュートリアルで解説したように、単純に対応するプロパティを定義し、
[propertyInfoForElement:]メソッドで、そのプロパティ名を返すようにしておけばよいです。

[size]要素下の[width]と[height]については、同じ方法ではうまくいきません。
[width]と[height]の親要素として、[size]要素が存在しているため、
imageエンティティには、[size]要素に対応するプロパティを定義する必要があります。

[size]要素に対応するプロパティには、CPXmlEntityクラスを継承する別のクラスを作成し、それを型として指定します。
まず、先にサイズ要素に対応するSizeEntityクラスを定義します。

[SizeEntity.h]
#import "CPXmlEntity.h"

@interface SizeEntity : CPXmlEntity

@property(strong,nonatomic) NSNumber *width;
@property(strong,nonatomic) NSNumber *height;

@end

[SizeEntity.m]
#import "SizeEntity.h"

@implementation SizeEntity

+ (id)propertyInfoForElement:(NSString *)element
{
    static NSDictionary *propDic;
    
    if(!propDic){
        
        propDic = @{
                    @"width": @"width",
                    @"height": @"height"
                    };
    }
    
    return propDic[element];
}

@end

そして、先に定義したSizeEntityを使用するImageEntityを定義します。

[ImageEntity.h]
#import "CPXmlEntity.h"
#import "SizeEntity.h"

@interface ImageEntity : CPXmlEntity

@property(strong,nonatomic) NSString *imageName;
@property(strong,nonatomic) SizeEntity *size;

@end

[ImageEntity.m]
#import "ImageEntity.h"

@implementation ImageEntity

+ (id)propertyInfoForElement:(NSString *)element
{
    static NSDictionary *propDic;
    
    if(!propDic){
        
        propDic = @{
                    @"image-name": @"imageName",
                    @"size": @"size"
                    };
    }
    
    return propDic[element];
}

@end


パース処理をする部分は特別な対応は必要はありません。

    
    NSData *data = [xmlText dataUsingEncoding:NSUTF8StringEncoding];
    ChimeraParser *chimeraParser = [[ChimeraParser alloc] initWithTargetClass:[ImageEntity class]];
    ImageEntity *image = [chimeraParser parse:data];
    NSLog(@"imageName = %@",image.imageName);
    NSLog(@"width = %@",image.size.width);
    NSLog(@"height = %@",image.size.height);

ChimeraXMLチュートリアル(1) シンプルなXMLを解析する
ChimeraXMLチュートリアル(2) エンティティ型のプロパティを定義する
ChimeraXML_チュートリアル(3) 配列型の要素を解析する

このドキュメントはObjective-C用のXMLデシリアライズライブラリ「ChimeraXML」のチュートリアルです。

シンプルなXMLの解析


以下のようなシンプルなXMLをChimeraXMLで解析してみます。

<person>
    <first-name>Taro</first-name>
    <last-name>Yamada</last-name>
    <age>22</age>
</person>

ChimeraXMLでXMLを解析する手順の大半は、デシリアライズ先となるオブジェクトのクラスを定義することです。
解析そのものは数行のコードで実行可能です。

デシリアライズ先となるオブジェクトのクラスはCPXmlEntityクラスを継承し、解析対象のXMLの要素に対応する
プロパティを持つクラスになります。クラス名にルールは有りません。

プロパティ名はXMLの要素名と一致させる必要はありません。プロパティ名と要素名のマッピング情報は、CPXmlEntityクラスの
クラスメソッド「propertyInfoForElement:」により、ChimeraXMLの解析エンジンに提供します。

CPXmlEntityを継承する各クラスは、propertyInfoForElement:」をオーバーライドする必要があります。

まず、上記サンプルXMLのデシリアライズとなるクラスのヘッダファイルを見てみましょう。

[PersonEntity.h]
#import "CPXmlEntity.h"

@interface PersonEntity : CPXmlEntity

@property(strong,nonatomic) NSString *firstName;
@property(strong,nonatomic) NSString *lastName;
@property(strong,nonatomic) NSNumber *age;

@end

XMLの要素、fist-name、last-name、ageに対応するプロパティfirstName、lastName、ageを定義しています。プロパティ名を
要素名に一致させる必要はありません。

ChimeraXMLで扱うことが出来るプロパティの型は、NSString、NSNumber、CPXmlEntityクラスを継承するクラス、NSArrayの4つです。
CPXmlEntityクラスを継承するクラス、NSArrayをプロパティとして使用する方法は後述します。

XMLが持つ全ての要素について、プロパティを定義する必要はありません。アプリケーションで必要な項目のみを
プロパティとして定義しておけば問題ありません。

次に実装クラスです。

[PersonEntity.m]
#import "PersonEntity.h"

@implementation PersonEntity

+ (id)propertyInfoForElement:(NSString *)element
{
    if([element isEqualToString:@"first-name"]) {
        return @"firstName";
    }
    else if([element isEqualToString:@"last-name"]) {
        return @"lastName";
    }
    else if([element isEqualToString:@"age"]) {
        return @"age";
    }
    
    return nil;
}

@end

CPXmlEntityの[propertyInfoForElement:]をオーバーライドし、XMLの要素に対応するプロパティの名前を返却しています。
戻り値がid型になっているのは、NSString型以外を返す必要があるケースがあるからです。(これについては後述します。)

XMLが持つ全ての要素、クラスが持つ全てのプロパティについて、このメソッドの戻り値を提供する必要はありません。
このメソッドが戻り値を提供しない(nilを返す)要素については、解析エンジンが無視をします。

上記のようにif文で要素名に応じて分岐を記述する方法でも問題ないですが、以下のようにNSDictionaryを使用することで
簡潔に記述することが可能です。

+ (id)propertyInfoForElement:(NSString *)element
{    
    static NSDictionary *propDic;
    
    if(!propDic){
        
        propDic = @{
                    @"first-name": @"firstName",
                    @"last-name": @"lastName",
                    @"age": @"age"
                    };
    }
    
    return propDic[element];
}

次に解析を実行する部分です。

NSData *data = [xmlText dataUsingEncoding:NSUTF8StringEncoding]; //xmlTextは解析対象のXMLを管理するNSString型変数
ChimeraParser *chimeraParser = [[ChimeraParser alloc] initWithTargetClass:[PersonEntity class]];
PersonEntity *person = [chimeraParser parse:data];

解析を実行するのはChimeraParserクラスです。「initWithTargetClass:」にデシリアライズ先のオブジェクトのクラスを指定して初期化します。
解析はparseメソッドにより行われます。戻り値として、initWithTargetClassに指定したクラスのオブジェクトが返却されます。
返されたオブジェクトのプロパティには、対応するXMLの要素の値が設定されています。

NSNumber型のプロパティに対応する要素に文字列以外の値が設定されている場合、プロパティには値が設定されません。


iOSでURLエンコーディングをする際、CoreFoundationのCFURLCreateStringByAddingPercentEscapes()を
使われている方は多いと思います。

しかし、ARC環境で、これを使う場合、注意しないとメモリリークを引き起こしてしまいます。
メモリリークを発生させない使い方は以下です。

NSString *encodedText = (__bridge_transfer NSString *)CFURLCreateStringByAddingPercentEscapes(
                                      NULL,
                                      (__bridge CFStringRef)sourceText,	//元の文字列
                                      NULL,
                                      CFSTR("!*'();:@&=+$,/?%#[]"),
                                      CFStringConvertNSStringEncodingToEncoding(NSUTF8StringEncoding));

ポイントは戻り値を(__bridge_transfer NSString *)でキャストしていることです。
(__bridge NSString *)でキャストするとメモリリークが発生します。
Createという単語を含むCoreFoundationの関数を使用する場合、戻り値のオブジェクトの所有権を取得します。つまり、
CFRelease()を後でコールする必要があるということです。

__bridge_transferは戻り値のオブジェクトの所有権をARCに移譲するためのキーワードです。
これにより、CFRelease()を自分でコールしなくても、ARCが面倒を見てくれるようになります。

__bridgeでキャストすると、ARCへの所有権の移譲が行われないため、自分でオブジェクトを解放する必要があります。